@milcho0604/velog-mcp 0.8.1 → 0.8.9
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 +216 -0
- package/README.ko.md +6 -5
- package/README.md +7 -6
- package/dist/format.d.ts +9 -0
- package/dist/format.js +12 -0
- package/dist/format.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/render/cover.js +19 -12
- package/dist/render/cover.js.map +1 -1
- package/dist/render/page.js +105 -15
- package/dist/render/page.js.map +1 -1
- package/dist/render/sequence.js +102 -26
- package/dist/render/sequence.js.map +1 -1
- package/dist/tools/drafts.js +5 -5
- package/dist/tools/drafts.js.map +1 -1
- package/dist/tools/images.js +3 -0
- package/dist/tools/images.js.map +1 -1
- package/dist/tools/publish.js +5 -5
- package/dist/tools/publish.js.map +1 -1
- package/docs/tools.md +4 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,214 @@
|
|
|
4
4
|
**무엇이 깨져 있었는지**를 먼저 적는다. 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/),
|
|
5
5
|
버전은 [유의적 버전](https://semver.org/lang/ko/)을 따른다.
|
|
6
6
|
|
|
7
|
+
## [0.8.9] — 2026-08-30
|
|
8
|
+
|
|
9
|
+
### 고침
|
|
10
|
+
|
|
11
|
+
- **팬아웃 꺾임 자리가 제각각이었다.** 같은 복도를 지나는 선을 무조건 서로
|
|
12
|
+
벌려서, 위로 가는 선과 아래로 가는 선처럼 겹칠 일이 없는 선까지 다른
|
|
13
|
+
자리에서 꺾였다. 세로(가로) 구간이 겹치는 선끼리만 다른 차선을 받는다.
|
|
14
|
+
- **차선이 여럿인 면의 정렬된 화살표가 살짝 기울었다.** 한 면에서 선 두 개가
|
|
15
|
+
나가면 차선 오프셋 때문에 같은 행 화살표도 몇 px 기울었다. 모은 자리가
|
|
16
|
+
옆 차선과 6px 이상 떨어지면 다차선이어도 수평으로 편다.
|
|
17
|
+
|
|
18
|
+
## [0.8.8] — 2026-08-30
|
|
19
|
+
|
|
20
|
+
### 고침
|
|
21
|
+
|
|
22
|
+
- **표지가 어디서나 나오는 자동 생성 포스터처럼 보였다.** 대각 그러데이션에 색 막대,
|
|
23
|
+
알약 태그. 구성도와 같은 시각 언어(흰 카드, 점 격자 배경, 배지 칩, 파스텔 태그 칩)로
|
|
24
|
+
다시 그렸다. 실제로 반응이 좋았던 글의 썸네일이 표지 카드가 아니라 본문 구성도였던
|
|
25
|
+
것이 근거다.
|
|
26
|
+
- 케커가 칩이 되면서 케커·서명 충돌 감사도 칩 폭 기준으로 본다.
|
|
27
|
+
|
|
28
|
+
### 추가
|
|
29
|
+
|
|
30
|
+
- **구성도 도구 설명에 입력 처방을 실었다.** 밋밋하게 나오면 렌더러가 아니라 입력이
|
|
31
|
+
밋밋한 것이다: 노드마다 icon 과 tag, 관련 노드는 groups, 흐름이 여럿이면 planes,
|
|
32
|
+
나란한 노드는 y 맞추기.
|
|
33
|
+
|
|
34
|
+
## [0.8.7] — 2026-08-30
|
|
35
|
+
|
|
36
|
+
### 고침
|
|
37
|
+
|
|
38
|
+
- **곧게 이을 수 있는 화살표가 가운데서 한 번 꺾였다.** 몇 px 어긋난 두 노드 사이도
|
|
39
|
+
정확히 중간에서 Z 로 꺾어 이어서, 나란히 놓은 구성도일수록 찌그러져 보였다.
|
|
40
|
+
이제 세로 구간이 겹치는 두 노드는 한 y 로 모아 완전한 직선으로 잇고,
|
|
41
|
+
구간이 안 겹쳐도 기울기가 완만하면(1:4 이하) 대각 직선으로 잇는다.
|
|
42
|
+
8px 이하 잔차는 꺾임 반경(9)보다 작아 잔물결이 되므로 그냥 곧게 잇는다.
|
|
43
|
+
- **같은 면의 차선을 배열 순서로 주고 있었다.** edges 배열에서 먼저 나온 선이
|
|
44
|
+
위 차선을 받아서, 마주 보는 타깃이 가장자리 차선에 걸리면 괜히 꺾이고
|
|
45
|
+
출발점 바로 앞에서 선끼리 교차했다. 상대편 위치 순서로 준다.
|
|
46
|
+
|
|
47
|
+
### 추가
|
|
48
|
+
|
|
49
|
+
- **쓰기 도구 4개의 본문·제목 설명에 사람 문체 안내를 실었다.** 긴 줄표(—)와
|
|
50
|
+
가운뎃점(·) 금지, 볼드 번호 나열 금지, 문단은 2~3문장, 부록 절 대신 본문에 녹이기.
|
|
51
|
+
사람이 쓴 통과본과 AI 초안을 나란히 세었을 때 실제로 갈린 항목만 담았다.
|
|
52
|
+
|
|
53
|
+
## [0.8.6] — 2026-08-27
|
|
54
|
+
|
|
55
|
+
코덱스 3차 검토(v0.8.4..HEAD) 지적 4건을 전부 반영했다. **0.8.5 에서 거짓 양성을
|
|
56
|
+
고치면서 이번엔 반대로 놓치게 만든 것**이 하나 있었다.
|
|
57
|
+
|
|
58
|
+
### 고침
|
|
59
|
+
|
|
60
|
+
- **걸친 노트를 감사에서 통째로 빼버렸다.** 0.8.5 에서 «걸친 노트는 그 구간을 덮는 게
|
|
61
|
+
설계» 라고 판단해 검사를 건너뛰게 했는데, 그러면 **제 구간 밖** 참가자의 막대를
|
|
62
|
+
덮어도 안 잡힌다. 이제 걸친 노트는 `lo~hi` **밖**만 본다. 막대 30겹에서 재현했다.
|
|
63
|
+
- **없는 막대 값을 물고 있었다.** 안 닫힌 막대는 그 참가자가 마지막으로 관여한
|
|
64
|
+
지점까지만 끈다(⑥의 `lastY`). 그러니 노트 뒤에 그 참가자가 안 나오면 막대는 노트
|
|
65
|
+
위에서 끝나 겹칠 일이 없다. 열린 개수만 세고 있어서, 없는 막대 때문에 노트가
|
|
66
|
+
반대편으로 뒤집히거나 간격이 헛되이 벌어졌다. 참가자별 마지막 등장 지점을 보고 판단한다.
|
|
67
|
+
|
|
68
|
+
### 검사
|
|
69
|
+
|
|
70
|
+
- **`BAR_NOTE_SAMPLE` 을 대조군에 넣었다.** 변이 표본을 대조군에 안 넣으면 원본부터
|
|
71
|
+
걸리는 상태가 되어도 «변이하니 걸렸다» 는 그대로 통과한다. 그럼 아무것도 증명 못 한다.
|
|
72
|
+
- **S10 이 `insetL` 을 한 번도 안 밟고 있었다.** 왼쪽 이웃 막대 회피가 동작으로
|
|
73
|
+
검증된 적이 없었다. 왼쪽에만 막대가 쌓이면 오른쪽으로 되돌아오는 사례를 넣었다.
|
|
74
|
+
|
|
75
|
+
### R16 상한을 3 으로
|
|
76
|
+
|
|
77
|
+
0.8.5 에서 4 → 2 로 조였는데 **CI Node 22.18 에서 3 이 찍혀 빨간불이 났다.**
|
|
78
|
+
프로필 두 개는 순차로 만들고 순차로 지우므로, 앞판 정리 도중과 뒷판 기동이
|
|
79
|
+
표본 추출 시점에 겹쳐 보인다. 3 은 여전히 «두 판(4개)» 과 구분되고, 직렬화를
|
|
80
|
+
무력화하는 변이에서 6 이 찍히는 것도 확인했다.
|
|
81
|
+
|
|
82
|
+
⚠️ 코덱스가 3차에서 «상한 2 가 간헐적으로 3 을 볼 수 있는지» 를 정확히 물었는데
|
|
83
|
+
**로컬 3회 통과로 넘겼다가 CI 에서 났다. 로컬 몇 회 통과는 근거가 안 된다.**
|
|
84
|
+
|
|
85
|
+
### ⚠️ 남는 한계
|
|
86
|
+
|
|
87
|
+
막대 30겹 이상이 쌓인 상태에서 그 구간 밖으로 걸친 노트를 두면 **감사가 거부한다.**
|
|
88
|
+
배치가 그걸 피하도록 넓히지는 않는다. 조용히 망가진 그림을 올리는 것보다 낫다고 보고
|
|
89
|
+
여기까지 뒀다. 실제로 30겹은 `return` 없이 호출만 서른 번 쌓아야 나온다.
|
|
90
|
+
|
|
91
|
+
## [0.8.5] — 2026-08-27
|
|
92
|
+
|
|
93
|
+
코덱스 2차 검토(v0.8.3..HEAD). **높음 6건, 중간 8건.** 그중 하나는 0.8.4 에서
|
|
94
|
+
**내가 만든 회귀**였다. 배포 직전에 잡았다.
|
|
95
|
+
|
|
96
|
+
### 🔴 고침 — 0.8.4 의 회귀
|
|
97
|
+
|
|
98
|
+
- **두 열에 걸친 노트가 무조건 감사에 걸렸다.** 0.8.4 에서 넣은 «노트가 활성막대를
|
|
99
|
+
덮는지» 검사가 **걸친 노트까지** 봤다. 걸친 노트는 그 구간을 덮는 것이 설계인데
|
|
100
|
+
(from~to 를 가리킨다면서 가운데만 떠 있으면 어디 얘기인지 모른다), 막대가 **1겹만**
|
|
101
|
+
열려 있어도 걸렸다. 멀쩡한 그림이 거부된다. 이제 **한 열짜리 노트만** 본다.
|
|
102
|
+
|
|
103
|
+
### 고침
|
|
104
|
+
|
|
105
|
+
- **왼쪽에 붙는 노트가 왼쪽 이웃의 막대를 안 비켰다.** 막대는 오른쪽으로만 자라므로
|
|
106
|
+
왼쪽은 안전하다고 봤는데, 왼쪽으로 가면 걸리는 상대가 **왼쪽 이웃**의 막대다.
|
|
107
|
+
`npadL` 에 그 몫을 넣었다.
|
|
108
|
+
- **R16 이 렌더 두 판의 동시 실행을 통과시켰다.** 프로필 상한을 4 로 뒀는데 한 판이
|
|
109
|
+
2개이므로 4는 곧 두 판이다. 그건 이 검사가 막으려던 상태다. 2 로 조였고 3회 연속 확인했다.
|
|
110
|
+
|
|
111
|
+
### 검사
|
|
112
|
+
|
|
113
|
+
- **S4 에 «노트가 막대 몫을 안 비킨다» 변이 추가.** S11 이 «문구가 소스에 있나» 만
|
|
114
|
+
보고 있어서 감사를 `if (false && ...)` 로 죽여도 통과했다(코덱스 지적). 진짜 변이로 옮겼다.
|
|
115
|
+
⚠️ 표본 조건 셋을 다 갖춰야 재현된다 — 첫 열(왼쪽 회피 불가), 깊은 막대, 노트 뒤 메시지.
|
|
116
|
+
- **S10 이 `inset` 을 0 으로만 부르고 있었다.** 그러면 `defR` 이 실수로 `npadL` 을 써도
|
|
117
|
+
두 값이 같아 전부 통과한다. 막대 몫이 붙으면 반대편을 고르는 사례를 넣었다.
|
|
118
|
+
|
|
119
|
+
### 재보고 넣지 않은 것
|
|
120
|
+
|
|
121
|
+
- 코덱스는 «노트 연결선이 막대와 교차하는 것을 감사가 놓친다» 고 했다. 눈으로 보니
|
|
122
|
+
**원래 지나갈 수밖에 없다** — 노트는 제 생명선에 붙어야 하고 막대가 그 사이에 있다.
|
|
123
|
+
감사에 넣으면 방금 고친 «걸친 노트» 와 같은 거짓 양성이 된다.
|
|
124
|
+
|
|
125
|
+
## [0.8.4] — 2026-08-27
|
|
126
|
+
|
|
127
|
+
0.8.3 의 «막대 몫» 수정이 **절반만 되어 있었다.** 배포해도 되는지 스스로 되묻고
|
|
128
|
+
안 본 자리를 훑다가 둘을 더 찾았다. 둘 다 0.8.3 에 있는 결함이다.
|
|
129
|
+
|
|
130
|
+
### 고침
|
|
131
|
+
|
|
132
|
+
- **자기호출이 막대 몫을 못 받았다.** `spans` 에만 넣었는데 자기호출은 그 경로를 안 탄다.
|
|
133
|
+
깊게 쌓인 막대 위에서 고리가 오른쪽으로 밀리는데 열은 안 넓어져 라벨이 제 구간을
|
|
134
|
+
넘었다(«깊은 막대 위 자기호출 + 긴 라벨» 에서 재현).
|
|
135
|
+
- **★ 노트가 활성 막대 위에 얹혔다.** 노트는 생명선에서 26px 만 띄우는데 막대는 깊이만큼
|
|
136
|
+
오른쪽으로 밀린다. 5겹부터 노트가 막대 밑에 깔리고 연결선도 가려졌다.
|
|
137
|
+
이제 오른쪽에 붙을 때만 막대 몫을 비켜준다 — 막대는 오른쪽으로만 밀리므로 왼쪽은 그대로다.
|
|
138
|
+
|
|
139
|
+
### 감사
|
|
140
|
+
|
|
141
|
+
- **노트가 제 활성막대를 덮는지 보게 했다.** 카드, 노트끼리, 남의 생명선은 보면서
|
|
142
|
+
**막대만 아무도 안 보고 있었다.** 위 결함이 감사를 통과한 이유다.
|
|
143
|
+
변이(상자 x 에서 막대 몫 제거)로 검출력을 확인했다.
|
|
144
|
+
⚠️ 표본을 고를 때 주의 — 노트가 왼쪽으로 피하거나 막대가 노트 위에서 끝나면
|
|
145
|
+
겹칠 일이 없어 변이가 무반응이다. **첫 열에 노트를 붙이고(왼쪽 회피 불가)
|
|
146
|
+
노트 뒤에 그 참가자가 한 번 더 등장해야**(막대가 아래로 이어짐) 재현된다.
|
|
147
|
+
|
|
148
|
+
### 검사
|
|
149
|
+
|
|
150
|
+
- **S11 에 3건 추가** — 자기호출 넓히기의 막대 몫, 노트의 막대 몫(오른쪽만),
|
|
151
|
+
감사에 노트 대 막대 검사가 있는지.
|
|
152
|
+
- 상자 x 계산을 한 줄(`nOff`)로 정리했다. S4 변이 표적과 S10 배선 검사도 따라 고쳤다.
|
|
153
|
+
- **★ R16 이 재는 대상을 바꿨다.** 크롬 **프로세스 개수**로 직렬화를 판정하고 있었는데,
|
|
154
|
+
한 판이 띄우는 헬퍼 수가 환경마다 달라 CI 에서 «바닥 0, 최고 15» 로 빨간불이 났다.
|
|
155
|
+
**직렬화는 멀쩡했다.** 개수로는 「한 판」과 「여러 판」을 못 가른다(이 검사가 이 이유로
|
|
156
|
+
깨진 게 두 번째다). 이제 **크롬 프로필 디렉터리**를 센다 — 렌더 1회가 정확히 2개를
|
|
157
|
+
만들고 `finally` 에서 지우므로 살아 있는 개수가 곧 동시 렌더 수 × 2 다.
|
|
158
|
+
직렬화를 무력화하는 변이에서 정확히 **6개(3판 × 2)** 가 찍히는 것을 확인했다.
|
|
159
|
+
|
|
160
|
+
## [0.8.3] — 2026-08-27
|
|
161
|
+
|
|
162
|
+
코덱스 검토(0.150.1) 지적 네 건 중 셋을 반영하고, 그 과정에서 **v0.7.4 부터 있던
|
|
163
|
+
결함 하나를 새로 찾아 고쳤다.** 지적 하나는 재보고 사실이 아니어서 넘겼다.
|
|
164
|
+
|
|
165
|
+
### 고침
|
|
166
|
+
|
|
167
|
+
- **★ 열 넓히기가 활성 막대 몫을 안 빼고 있었다.** 화살표는 생명선 한가운데가 아니라
|
|
168
|
+
막대 가장자리에서 끊는데, 막대가 쌓인 구간은 라벨이 쓸 가로가 그만큼 줄어든다.
|
|
169
|
+
`call` 만 12개 이어 붙이고 긴 라벨을 주면 «라벨이 제 구간 밖으로 나감» 으로
|
|
170
|
+
**감사에 걸려 업로드가 거부됐다.** 짝을 맞춰 막대를 닫으면 통과했고 번호와는
|
|
171
|
+
무관했다. v0.7.4, v0.8.0, v0.8.1 전부 같은 실패라 이번 변경의 회귀가 아니다.
|
|
172
|
+
막대 깊이는 y 좌표와 무관하게 메시지 순서만으로 정해지므로, 열 간격을 정하기 전에
|
|
173
|
+
미리 세어 반영한다.
|
|
174
|
+
- **한 열짜리(노트, 자기호출) 넓히기를 `spans` 뒤로 옮겼다.** 노트를 어느 쪽에 붙일지는
|
|
175
|
+
그때의 간격을 보고 정하는데, `spans` 앞에서 정하면 곧 넓어질 쪽을 모르고 골라
|
|
176
|
+
반대쪽을 쓸데없이 벌린다. 실측 **859 → 720**. `spans` 는 간격을 늘리기만 하므로
|
|
177
|
+
뒤에서 더 넓혀도 그 조건은 그대로 성립한다.
|
|
178
|
+
- `docs/tools.md` 가 아직 «노트는 그 열 오른쪽에 붙는다» 라고 적고 있었다. 0.8.1 부터
|
|
179
|
+
덜 벌리는 쪽에 붙는다. 결정 규칙까지 적었다.
|
|
180
|
+
|
|
181
|
+
### 검사
|
|
182
|
+
|
|
183
|
+
- **S11** — 막대 몫 계산(`insetOf`)과 그것을 `spans` 가 실제로 더하는지.
|
|
184
|
+
- **S12** — 소스에서 `spans` 가 한 열짜리 넓히기보다 앞에 오는지.
|
|
185
|
+
- **S9 보강** — 폭(`m.tw`)을 번호 붙인 **뒤에** 재는지. 순서를 바꿔도 기존 S9 가 전부
|
|
186
|
+
통과했다(코덱스 지적). 이제 순서를 못 박고 변이로 확인했다.
|
|
187
|
+
- **P28 보강** — 계열 최댓값만 보면 가운데가 빠져도 통과한다(코덱스 지적).
|
|
188
|
+
S9 를 지워도 S10 이 남아 최댓값은 10 이다. 이제 빠진 번호를 같이 본다.
|
|
189
|
+
|
|
190
|
+
### 재보고 넘긴 것
|
|
191
|
+
|
|
192
|
+
- 코덱스는 «번호 때문에 첫 줄이 WRAP_W 를 넘는데 열 간격이 반영을 안 한다» 고 했으나,
|
|
193
|
+
소스는 이미 번호를 붙인 **뒤에** 폭을 재고 있었다. 지적이 맞았던 건 **검사 쪽**이라
|
|
194
|
+
거기만 보강했다.
|
|
195
|
+
|
|
196
|
+
## [0.8.2] — 2026-08-27
|
|
197
|
+
|
|
198
|
+
### 문서
|
|
199
|
+
|
|
200
|
+
- **README 두 판이 낡은 검증 수치를 적고 있었다.** «테스트 276건, R1~R23, P1~P26»
|
|
201
|
+
이라고 했는데 실제로는 416건이었고 `render.test.ts` 에 **S1~S10 이 통째로 더** 있었다.
|
|
202
|
+
0.7.0 에서 시퀀스 검사 열 개를 넣으면서 README 를 안 고쳤다.
|
|
203
|
+
이 저장소는 같은 이유로 한 번 지적받은 적이 있다(`d59d3dc`, 코덱스 9차).
|
|
204
|
+
|
|
205
|
+
### 검사
|
|
206
|
+
|
|
207
|
+
- **P28** — README 두 판이 말하는 검사 계열 범위가 실제와 맞는지 본다. 소스에서
|
|
208
|
+
`A`, `R`, `S`, `P` 계열의 최댓값을 긁어 한글판(`~`)과 영문판(`–`) 표기를 대조한다.
|
|
209
|
+
숫자를 손으로 맞추는 한 또 낡으므로 **계열 범위만이라도 기계가 지키게** 했다.
|
|
210
|
+
양방향 변이로 확인했다 — README 를 낡게 만들어도, 검사만 늘리고 문서를 안 고쳐도 걸린다.
|
|
211
|
+
- 총 건수는 기계가 안 본다. 변이 루프처럼 **실행 중에 만들어지는 검사**가 있어 소스만
|
|
212
|
+
읽어서는 셀 수 없다. 그래서 README 에는 «0.8.2 기준» 이라고 시점을 함께 적었다.
|
|
213
|
+
- ⚠️ «소스 변이 54종» 은 손으로 관리하는 수치라 이번에 손대지 않았다. 확인할 방법이 없다.
|
|
214
|
+
|
|
7
215
|
## [0.8.1] — 2026-08-27
|
|
8
216
|
|
|
9
217
|
### 고침
|
|
@@ -224,6 +432,14 @@
|
|
|
224
432
|
|
|
225
433
|
- 첫 npm 공개 발행.
|
|
226
434
|
|
|
435
|
+
[0.8.9]: https://github.com/milcho0604/velog-mcp/releases/tag/v0.8.9
|
|
436
|
+
[0.8.8]: https://github.com/milcho0604/velog-mcp/releases/tag/v0.8.8
|
|
437
|
+
[0.8.7]: https://github.com/milcho0604/velog-mcp/releases/tag/v0.8.7
|
|
438
|
+
[0.8.6]: https://github.com/milcho0604/velog-mcp/releases/tag/v0.8.6
|
|
439
|
+
[0.8.5]: https://github.com/milcho0604/velog-mcp/releases/tag/v0.8.5
|
|
440
|
+
[0.8.4]: https://github.com/milcho0604/velog-mcp/releases/tag/v0.8.4
|
|
441
|
+
[0.8.3]: https://github.com/milcho0604/velog-mcp/releases/tag/v0.8.3
|
|
442
|
+
[0.8.2]: https://github.com/milcho0604/velog-mcp/releases/tag/v0.8.2
|
|
227
443
|
[0.8.1]: https://github.com/milcho0604/velog-mcp/releases/tag/v0.8.1
|
|
228
444
|
[0.8.0]: https://github.com/milcho0604/velog-mcp/releases/tag/v0.8.0
|
|
229
445
|
[0.7.4]: https://github.com/milcho0604/velog-mcp/releases/tag/v0.7.4
|
package/README.ko.md
CHANGED
|
@@ -63,7 +63,7 @@ npm 에 올려뒀으니 클론할 것 없이 MCP 클라이언트가 `npx` 로
|
|
|
63
63
|
|
|
64
64
|
```bash
|
|
65
65
|
claude mcp add velog -e VELOG_REFRESH_TOKEN=여기에_토큰 \
|
|
66
|
-
-- npx -y @milcho0604/velog-mcp@0.8.
|
|
66
|
+
-- npx -y @milcho0604/velog-mcp@0.8.9
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
이 방식은 토큰이 클라이언트 설정 파일에 남는다. 위의 플러그인 방식은 키체인에 넣는다.
|
|
@@ -85,7 +85,7 @@ MCP 클라이언트 설정 파일(`claude_desktop_config.json`, `.mcp.json` 등)
|
|
|
85
85
|
"mcpServers": {
|
|
86
86
|
"velog": {
|
|
87
87
|
"command": "npx",
|
|
88
|
-
"args": ["-y", "@milcho0604/velog-mcp@0.8.
|
|
88
|
+
"args": ["-y", "@milcho0604/velog-mcp@0.8.9"],
|
|
89
89
|
"env": {
|
|
90
90
|
"VELOG_REFRESH_TOKEN": "여기에 토큰"
|
|
91
91
|
}
|
|
@@ -98,7 +98,7 @@ Claude Code CLI 라면:
|
|
|
98
98
|
|
|
99
99
|
```bash
|
|
100
100
|
claude mcp add velog -e VELOG_REFRESH_TOKEN=여기에_토큰 \
|
|
101
|
-
-- npx -y @milcho0604/velog-mcp@0.8.
|
|
101
|
+
-- npx -y @milcho0604/velog-mcp@0.8.9
|
|
102
102
|
```
|
|
103
103
|
|
|
104
104
|
로컬 체크아웃으로 돌리려면 command 를
|
|
@@ -393,8 +393,9 @@ npm run build # tsconfig.build.json (dist 에 테스트 미포함)
|
|
|
393
393
|
npm run schema:dump # 현재 벨로그 GraphQL 스키마 덤프
|
|
394
394
|
```
|
|
395
395
|
|
|
396
|
-
테스트
|
|
397
|
-
|
|
396
|
+
테스트 431건(0.8.9 기준). `safety.test.ts` 가 보안 불변식(A1~A12)을,
|
|
397
|
+
`render.test.ts` 가 구성도 불변식(R1~R23, D1)과 시퀀스 불변식(S1~S12)을,
|
|
398
|
+
`plugin.test.ts` 가 포장 불변식(P1~P28)을 고정한다.
|
|
398
399
|
깨지면 우회하지 말고 왜 깨졌는지부터 볼 것.
|
|
399
400
|
|
|
400
401
|
여기 있는 방어는 전부 **일부러 망가뜨려** 확인했다. 소스 변이 54종 + 발행 관문
|
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ Published on npm, so nothing to clone — your MCP client runs it via `npx`. See
|
|
|
68
68
|
|
|
69
69
|
```bash
|
|
70
70
|
claude mcp add velog -e VELOG_REFRESH_TOKEN=your_refresh_token \
|
|
71
|
-
-- npx -y @milcho0604/velog-mcp@0.8.
|
|
71
|
+
-- npx -y @milcho0604/velog-mcp@0.8.9
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
The token stays in your client's config file here. The plugin route above puts it in
|
|
@@ -91,7 +91,7 @@ Add this to your MCP client config (`claude_desktop_config.json`, `.mcp.json`,
|
|
|
91
91
|
"mcpServers": {
|
|
92
92
|
"velog": {
|
|
93
93
|
"command": "npx",
|
|
94
|
-
"args": ["-y", "@milcho0604/velog-mcp@0.8.
|
|
94
|
+
"args": ["-y", "@milcho0604/velog-mcp@0.8.9"],
|
|
95
95
|
"env": {
|
|
96
96
|
"VELOG_REFRESH_TOKEN": "your_refresh_token"
|
|
97
97
|
}
|
|
@@ -104,7 +104,7 @@ With the Claude Code CLI:
|
|
|
104
104
|
|
|
105
105
|
```bash
|
|
106
106
|
claude mcp add velog -e VELOG_REFRESH_TOKEN=your_refresh_token \
|
|
107
|
-
-- npx -y @milcho0604/velog-mcp@0.8.
|
|
107
|
+
-- npx -y @milcho0604/velog-mcp@0.8.9
|
|
108
108
|
```
|
|
109
109
|
|
|
110
110
|
To run a local checkout instead, swap the command for
|
|
@@ -406,9 +406,10 @@ npm run build # tsconfig.build.json (tests excluded from dist)
|
|
|
406
406
|
npm run schema:dump # dump Velog's current GraphQL schema
|
|
407
407
|
```
|
|
408
408
|
|
|
409
|
-
|
|
410
|
-
`render.test.ts` pins the
|
|
411
|
-
|
|
409
|
+
431 tests (as of 0.8.9). `src/__tests__/safety.test.ts` pins the security
|
|
410
|
+
invariants (A1–A12), `render.test.ts` pins the diagram ones (R1–R23, D1) and the
|
|
411
|
+
sequence ones (S1–S12), and `plugin.test.ts` pins the packaging ones (P1–P28) —
|
|
412
|
+
if any fails, find out why instead of working around it.
|
|
412
413
|
|
|
413
414
|
Every guard here was checked by **breaking it on purpose**: 54 mutations against the
|
|
414
415
|
source, plus 12 against the publish gate itself (`scripts/gate-mutation.sh`), each of
|
package/dist/format.d.ts
CHANGED
|
@@ -5,6 +5,15 @@
|
|
|
5
5
|
* 정작 필요한 값(제목·슬러그·조회수)은 묻힌다. 목록은 표로, 단건은 본문 위주로 낸다.
|
|
6
6
|
*/
|
|
7
7
|
import type { VelogPostSummary, VelogPostDetail } from './types.ts';
|
|
8
|
+
/**
|
|
9
|
+
* 글 본문 문체 안내. 쓰기 도구 4개의 body 설명에 공통으로 붙는다.
|
|
10
|
+
*
|
|
11
|
+
* 근거는 실측이다: 사람이 쓴 통과본과 AI 초안을 나란히 세었을 때 실제로 갈린
|
|
12
|
+
* 항목만 담았다(볼드 밀도·표·이모지는 차이가 없어서 넣지 않는다).
|
|
13
|
+
*/
|
|
14
|
+
export declare const HUMAN_BODY_STYLE: string;
|
|
15
|
+
/** 제목 규칙. 제목 필드 설명에 붙는다. */
|
|
16
|
+
export declare const HUMAN_TITLE_STYLE = " \uAE34 \uC904\uD45C(\u2014) \uAE08\uC9C0. \uBD80\uC81C\uB294 \uCF5C\uB860\uC774\uB098 \uAD04\uD638, \uC9E7\uC740 \uD558\uC774\uD508\uC73C\uB85C.";
|
|
8
17
|
export declare function postUrl(post: VelogPostSummary): string;
|
|
9
18
|
/** title 도 nullable 이라 표시 전에 한 번 거른다. */
|
|
10
19
|
export declare function postTitle(post: VelogPostSummary): string;
|
package/dist/format.js
CHANGED
|
@@ -4,6 +4,18 @@
|
|
|
4
4
|
* MCP 도구는 결국 모델이 읽는다. 원본 JSON 을 그대로 던지면 토큰만 먹고
|
|
5
5
|
* 정작 필요한 값(제목·슬러그·조회수)은 묻힌다. 목록은 표로, 단건은 본문 위주로 낸다.
|
|
6
6
|
*/
|
|
7
|
+
/**
|
|
8
|
+
* 글 본문 문체 안내. 쓰기 도구 4개의 body 설명에 공통으로 붙는다.
|
|
9
|
+
*
|
|
10
|
+
* 근거는 실측이다: 사람이 쓴 통과본과 AI 초안을 나란히 세었을 때 실제로 갈린
|
|
11
|
+
* 항목만 담았다(볼드 밀도·표·이모지는 차이가 없어서 넣지 않는다).
|
|
12
|
+
*/
|
|
13
|
+
export const HUMAN_BODY_STYLE = ' 문체: 사람이 쓴 글처럼 자연스럽게. 긴 줄표(—)와 가운뎃점(·)을 쓰지 말고 쉼표나 마침표로 끊을 것.' +
|
|
14
|
+
' "**하나.** ... **둘.**" 식 볼드 번호 나열 금지(산문이나 평범한 불릿으로).' +
|
|
15
|
+
' 문단은 2~3문장으로 짧게, 200자가 넘으면 쪼갠다.' +
|
|
16
|
+
' "이 글의 한계" 같은 부록 절을 만들지 말고 해당 문단 자리에 한 문장으로 녹일 것.';
|
|
17
|
+
/** 제목 규칙. 제목 필드 설명에 붙는다. */
|
|
18
|
+
export const HUMAN_TITLE_STYLE = ' 긴 줄표(—) 금지. 부제는 콜론이나 괄호, 짧은 하이픈으로.';
|
|
7
19
|
export function postUrl(post) {
|
|
8
20
|
const username = post.user?.username;
|
|
9
21
|
// url_slug 는 공식 스키마상 nullable 이다.
|
package/dist/format.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format.js","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"format.js","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
10
10
|
import { type Capabilities } from './capabilities.ts';
|
|
11
11
|
import { VelogClient } from './client.ts';
|
|
12
12
|
export declare const SERVER_NAME = "velog-mcp";
|
|
13
|
-
export declare const SERVER_VERSION = "0.8.
|
|
13
|
+
export declare const SERVER_VERSION = "0.8.9";
|
|
14
14
|
export declare function createServer(client: VelogClient, capabilities?: Capabilities): McpServer;
|
|
15
15
|
/**
|
|
16
16
|
* 크롬이 있어야만 되는 도구들.
|
package/dist/index.js
CHANGED
|
@@ -28,7 +28,7 @@ import { registerImageTools } from "./tools/images.js";
|
|
|
28
28
|
export const SERVER_NAME = 'velog-mcp';
|
|
29
29
|
// ⚠️ 이 값은 네 곳이 함께 움직인다 — package.json / plugin.json / .mcp.json 의
|
|
30
30
|
// npx 핀 / 여기. 어긋나면 P8 테스트가 잡는다. 손으로 맞추지 말고 테스트를 믿을 것.
|
|
31
|
-
export const SERVER_VERSION = '0.8.
|
|
31
|
+
export const SERVER_VERSION = '0.8.9';
|
|
32
32
|
export function createServer(client, capabilities = { publicPublish: false, editProfile: false }) {
|
|
33
33
|
const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION }, {
|
|
34
34
|
instructions: [
|
package/dist/render/cover.js
CHANGED
|
@@ -46,15 +46,18 @@ function txt(x, y, s, size, weightCls, fill, anchor){
|
|
|
46
46
|
return t;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// 구성도와 같은 시각 언어로 그린다: 흰 카드, 점 격자, 배지 칩.
|
|
50
|
+
// 그러데이션 포스터 풍은 어디서나 나오는 자동 생성 표지처럼 읽혀서 버렸다.
|
|
49
51
|
var defs = el('defs', {});
|
|
50
|
-
var
|
|
51
|
-
el('
|
|
52
|
-
el('
|
|
53
|
-
el('rect', {x:0, y:0, width:W, height:H, fill:'
|
|
54
|
-
el('rect', {x:0, y:0, width:
|
|
55
|
-
|
|
56
|
-
el('
|
|
57
|
-
|
|
52
|
+
var pat = el('pattern', {id:'dots', width:26, height:26, patternUnits:'userSpaceOnUse'}, defs);
|
|
53
|
+
el('circle', {cx:3, cy:3, r:1.6, fill:S.solid, opacity:'0.35'}, pat);
|
|
54
|
+
el('rect', {x:0, y:0, width:W, height:H, fill:'#ffffff'});
|
|
55
|
+
el('rect', {x:0, y:0, width:W, height:H, fill:S.fill, opacity:'0.45'});
|
|
56
|
+
el('rect', {x:0, y:0, width:W, height:H, fill:'url(#dots)'});
|
|
57
|
+
var CARD = 40;
|
|
58
|
+
el('rect', {x:CARD, y:CARD, width:W - CARD*2, height:H - CARD*2, rx:18, fill:'#ffffff',
|
|
59
|
+
stroke:'#e5e7eb', 'stroke-width':1.4,
|
|
60
|
+
filter:'drop-shadow(0 2px 6px rgba(15,23,42,.08))'});
|
|
58
61
|
|
|
59
62
|
var scratch = el('g', {visibility:'hidden'});
|
|
60
63
|
function widthOf(s, size, cls){
|
|
@@ -94,7 +97,11 @@ function wrap(s, size, cls, maxW, maxLines){
|
|
|
94
97
|
var truncated = [];
|
|
95
98
|
var bare = function(v){ return v.replace(/\\s/g, ''); };
|
|
96
99
|
|
|
97
|
-
if (S.kicker)
|
|
100
|
+
if (S.kicker) {
|
|
101
|
+
var kcw = widthOf(S.kicker, 19, 'kicker') + 28;
|
|
102
|
+
el('rect', {x:M, y:M - 20, width:kcw, height:40, rx:9, fill:S.solid});
|
|
103
|
+
txt(M + kcw/2, M + 7, S.kicker, 19, 'kicker', '#ffffff', 'middle');
|
|
104
|
+
}
|
|
98
105
|
|
|
99
106
|
// 제목: 3줄에 안 들어가면 글자 크기를 줄여 다시 시도한다
|
|
100
107
|
var size = 62, lines = [];
|
|
@@ -137,8 +144,8 @@ if (S.tags && S.tags.length) {
|
|
|
137
144
|
var label = '#' + S.tags[ti];
|
|
138
145
|
var tw = widthOf(label, 19, 'tag') + 30;
|
|
139
146
|
if (tx + tw > W - M) { truncated.push('태그 ' + label); continue; }
|
|
140
|
-
el('rect', {x:tx, y:ty - 25, width:tw, height:38, rx:
|
|
141
|
-
stroke:S.stroke, 'stroke-width':1.
|
|
147
|
+
el('rect', {x:tx, y:ty - 25, width:tw, height:38, rx:10, fill:S.fill,
|
|
148
|
+
stroke:S.stroke, 'stroke-width':1.2});
|
|
142
149
|
var tt = txt(tx + tw/2, ty, label, 19, 'tag', S.solid, 'middle');
|
|
143
150
|
tx += tw + 12;
|
|
144
151
|
}
|
|
@@ -149,7 +156,7 @@ if (S.footer) txt(W - M, M + 8, S.footer, 19, 'foot', '#94a3b8', 'end');
|
|
|
149
156
|
// ★ 상단 라벨(왼쪽)과 서명(오른쪽)은 같은 높이에 고정 배치된다. 둘 다 길면 가운데서
|
|
150
157
|
// 부딪히는데 여기엔 줄바꿈도 축소도 없다 — 감사만이라도 해야 조용히 안 나간다.
|
|
151
158
|
if (S.kicker || S.footer) {
|
|
152
|
-
var kw = S.kicker ? widthOf(S.kicker,
|
|
159
|
+
var kw = S.kicker ? widthOf(S.kicker, 19, 'kicker') + 28 : 0;
|
|
153
160
|
var fw = S.footer ? widthOf(S.footer, 19, 'foot') : 0;
|
|
154
161
|
if (M + kw > W - M - fw - 20) truncated.push('상단 라벨과 서명이 겹침');
|
|
155
162
|
}
|
package/dist/render/cover.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cover.js","sourceRoot":"","sources":["../../src/render/cover.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAsBlC,MAAM,KAAK,GAAG;;;;;;;;;;;CAWb,CAAC;AAEF,iDAAiD;AACjD,MAAM,MAAM,GAAG
|
|
1
|
+
{"version":3,"file":"cover.js","sourceRoot":"","sources":["../../src/render/cover.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAsBlC,MAAM,KAAK,GAAG;;;;;;;;;;;CAWb,CAAC;AAEF,iDAAiD;AACjD,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+Id,CAAC;AAEF,SAAS,QAAQ,CAAC,KAAc;IAC/B,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;AACvD,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAe;IAC7C,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAClC,MAAM,OAAO,GAAG;QACf,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,EAAE;QAC7B,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,EAAE;QACzB,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,EAAE;QACrB,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,EAAE;QACzB,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,KAAK,EAAE,CAAC,CAAC,KAAK;KACd,CAAC;IACF,OAAO;QACN,iBAAiB;QACjB,uEAAuE;QACvE,UAAU,KAAK,UAAU;QACzB,eAAe;QACf,6EAA6E;QAC7E,6CAA6C,QAAQ,CAAC,OAAO,CAAC,WAAW;QACzE,WAAW,MAAM,WAAW;QAC5B,gBAAgB;KAChB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACd,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,CAAa;IAC7C,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,CAAC,CAAC,SAAS,CAAC,MAAM;QAAE,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC1E,IAAI,CAAC,CAAC,SAAS,GAAG,EAAE;QAAE,KAAK,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,SAAS,YAAY,CAAC,CAAC;IAC3E,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,oBAAoB,CAAC;AACxE,CAAC"}
|
package/dist/render/page.js
CHANGED
|
@@ -287,13 +287,15 @@ function isH(side){ return side === 'left' || side === 'right'; }
|
|
|
287
287
|
function route(a, as, b, bs, off){
|
|
288
288
|
var ax = a[0], ay = a[1], bx = b[0], by = b[1];
|
|
289
289
|
var d = off || 0;
|
|
290
|
+
// 8px 이하 어긋남은 꺾지 않고 곧게 잇는다. 꺾임 반경(9)보다 작아서
|
|
291
|
+
// Z 로 꺾으면 직각이 아니라 잔물결이 된다.
|
|
290
292
|
if (isH(as) && isH(bs)) {
|
|
291
|
-
if (Math.abs(ay - by)
|
|
293
|
+
if (Math.abs(ay - by) <= 8) return [[ax,ay],[bx,by]];
|
|
292
294
|
var mx = (ax + bx) / 2 + d;
|
|
293
295
|
return [[ax,ay],[mx,ay],[mx,by],[bx,by]];
|
|
294
296
|
}
|
|
295
297
|
if (!isH(as) && !isH(bs)) {
|
|
296
|
-
if (Math.abs(ax - bx)
|
|
298
|
+
if (Math.abs(ax - bx) <= 8) return [[ax,ay],[bx,by]];
|
|
297
299
|
var my = (ay + by) / 2 + d;
|
|
298
300
|
return [[ax,ay],[ax,my],[bx,my],[bx,by]];
|
|
299
301
|
}
|
|
@@ -341,17 +343,81 @@ for (var ei = 0; ei < S.edges.length; ei++) {
|
|
|
341
343
|
lanes[kb] = (lanes[kb] || 0) + 1;
|
|
342
344
|
plan.push({e:e, pts:null, a:{n:na, s:as, k:ka}, b:{n:nb, s:bs, k:kb}});
|
|
343
345
|
}
|
|
344
|
-
var seen = Object.create(null);
|
|
345
346
|
var auto = [];
|
|
346
347
|
for (var pj = 0; pj < plan.length; pj++) {
|
|
347
348
|
var it = plan[pj];
|
|
348
349
|
if (it.pts || !it.a) continue;
|
|
349
|
-
seen[it.a.k] = (seen[it.a.k] === undefined) ? 0 : seen[it.a.k] + 1;
|
|
350
|
-
seen[it.b.k] = (seen[it.b.k] === undefined) ? 0 : seen[it.b.k] + 1;
|
|
351
|
-
it.pa = anchor(it.a.n, it.a.s, seen[it.a.k], lanes[it.a.k]);
|
|
352
|
-
it.pb = anchor(it.b.n, it.b.s, seen[it.b.k], lanes[it.b.k]);
|
|
353
350
|
auto.push(it);
|
|
354
351
|
}
|
|
352
|
+
// 같은 면의 차선은 배열 순서가 아니라 **상대편 위치 순서**로 준다.
|
|
353
|
+
// 순서대로 주면 마주 보는 타깃이 가장자리 차선에 걸려 괜히 꺾이고,
|
|
354
|
+
// 출발점 바로 앞에서 선끼리 교차한다.
|
|
355
|
+
var laneBuckets = Object.create(null);
|
|
356
|
+
for (var lb = 0; lb < auto.length; lb++) {
|
|
357
|
+
var itb = auto[lb];
|
|
358
|
+
(laneBuckets[itb.a.k] = laneBuckets[itb.a.k] || []).push({it:itb, end:'a'});
|
|
359
|
+
(laneBuckets[itb.b.k] = laneBuckets[itb.b.k] || []).push({it:itb, end:'b'});
|
|
360
|
+
}
|
|
361
|
+
for (var lk in laneBuckets) {
|
|
362
|
+
var arr = laneBuckets[lk];
|
|
363
|
+
var hSide = isH(arr[0].end === 'a' ? arr[0].it.a.s : arr[0].it.b.s);
|
|
364
|
+
arr.sort(function(x, y){
|
|
365
|
+
var nx = x.end === 'a' ? x.it.b.n : x.it.a.n;
|
|
366
|
+
var ny = y.end === 'a' ? y.it.b.n : y.it.a.n;
|
|
367
|
+
return hSide ? (cy(nx) - cy(ny)) : (cx(nx) - cx(ny));
|
|
368
|
+
});
|
|
369
|
+
for (var li = 0; li < arr.length; li++) {
|
|
370
|
+
if (arr[li].end === 'a') arr[li].it.ia = li; else arr[li].it.ib = li;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
for (var pa2 = 0; pa2 < auto.length; pa2++) {
|
|
374
|
+
var ita = auto[pa2];
|
|
375
|
+
ita.pa = anchor(ita.a.n, ita.a.s, ita.ia, lanes[ita.a.k]);
|
|
376
|
+
ita.pb = anchor(ita.b.n, ita.b.s, ita.ib, lanes[ita.b.k]);
|
|
377
|
+
}
|
|
378
|
+
// 양끝이 가로면이고 두 노드의 세로 구간이 겹치면 한 y 로 모아 곧게 편다.
|
|
379
|
+
// 몇 px 어긋난 노드 사이를 가운데서 꺾는 것보다 곧은 한 줄이 낫다.
|
|
380
|
+
// 구간이 안 겹쳐도 기울기가 완만하면(1:4 이하) 대각 직선으로 잇는다.
|
|
381
|
+
// 차선이 여럿인 면도 편다 — 단, 모은 자리가 옆 차선과 6px 이상 떨어질 때만.
|
|
382
|
+
function laneClear(key, self, axis, v){
|
|
383
|
+
var arr = laneBuckets[key] || [];
|
|
384
|
+
for (var ci = 0; ci < arr.length; ci++) {
|
|
385
|
+
if (arr[ci].it === self) continue;
|
|
386
|
+
var pt = arr[ci].end === 'a' ? arr[ci].it.pa : arr[ci].it.pb;
|
|
387
|
+
if (Math.abs(pt[axis] - v) < 6) return false;
|
|
388
|
+
}
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
for (var sn = 0; sn < auto.length; sn++) {
|
|
392
|
+
var its = auto[sn];
|
|
393
|
+
var single = lanes[its.a.k] === 1 && lanes[its.b.k] === 1;
|
|
394
|
+
var hh = isH(its.a.s) && isH(its.b.s);
|
|
395
|
+
var vv = !isH(its.a.s) && !isH(its.b.s);
|
|
396
|
+
var M = 10;
|
|
397
|
+
if (hh) {
|
|
398
|
+
var lo = Math.max(its.a.n.y, its.b.n.y) + M;
|
|
399
|
+
var hi = Math.min(its.a.n.y + its.a.n.h, its.b.n.y + its.b.n.h) - M;
|
|
400
|
+
if (lo <= hi) {
|
|
401
|
+
var sy = Math.min(hi, Math.max(lo, (its.pa[1] + its.pb[1]) / 2));
|
|
402
|
+
if (laneClear(its.a.k, its, 1, sy) && laneClear(its.b.k, its, 1, sy)) {
|
|
403
|
+
its.pa[1] = sy; its.pb[1] = sy;
|
|
404
|
+
}
|
|
405
|
+
} else if (single && Math.abs(its.pb[1] - its.pa[1]) * 4 <= Math.abs(its.pb[0] - its.pa[0])) {
|
|
406
|
+
its.straight = true;
|
|
407
|
+
}
|
|
408
|
+
} else if (vv) {
|
|
409
|
+
var lo2 = Math.max(its.a.n.x, its.b.n.x) + M;
|
|
410
|
+
var hi2 = Math.min(its.a.n.x + its.a.n.w, its.b.n.x + its.b.n.w) - M;
|
|
411
|
+
if (lo2 <= hi2) {
|
|
412
|
+
var sx = Math.min(hi2, Math.max(lo2, (its.pa[0] + its.pb[0]) / 2));
|
|
413
|
+
if (laneClear(its.a.k, its, 0, sx) && laneClear(its.b.k, its, 0, sx)) {
|
|
414
|
+
its.pa[0] = sx; its.pb[0] = sx;
|
|
415
|
+
}
|
|
416
|
+
} else if (single && Math.abs(its.pb[0] - its.pa[0]) * 4 <= Math.abs(its.pb[1] - its.pa[1])) {
|
|
417
|
+
its.straight = true;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
355
421
|
|
|
356
422
|
// ★ 같은 두 열 사이를 지나는 선들은 중간 꺾임 좌표가 전부 같아서 한 줄로 겹친다.
|
|
357
423
|
// 노드 배치가 규칙적일수록(= 보기 좋게 그릴수록) 더 잘 생긴다.
|
|
@@ -359,11 +425,11 @@ for (var pj = 0; pj < plan.length; pj++) {
|
|
|
359
425
|
function midKeyOf(it){
|
|
360
426
|
var hA = isH(it.a.s), hB = isH(it.b.s);
|
|
361
427
|
if (hA && hB) {
|
|
362
|
-
if (Math.abs(it.pa[1] - it.pb[1])
|
|
428
|
+
if (Math.abs(it.pa[1] - it.pb[1]) <= 8) return ''; // 일직선 — 꺾임 없음
|
|
363
429
|
return 'h' + Math.round(((it.pa[0] + it.pb[0]) / 2) / 8);
|
|
364
430
|
}
|
|
365
431
|
if (!hA && !hB) {
|
|
366
|
-
if (Math.abs(it.pa[0] - it.pb[0])
|
|
432
|
+
if (Math.abs(it.pa[0] - it.pb[0]) <= 8) return '';
|
|
367
433
|
return 'v' + Math.round(((it.pa[1] + it.pb[1]) / 2) / 8);
|
|
368
434
|
}
|
|
369
435
|
return ''; // ㄴ자 — 중간선이 없다
|
|
@@ -373,15 +439,39 @@ for (var m1 = 0; m1 < auto.length; m1++) {
|
|
|
373
439
|
auto[m1].mk = midKeyOf(auto[m1]);
|
|
374
440
|
if (auto[m1].mk) midCount[auto[m1].mk] = (midCount[auto[m1].mk] || 0) + 1;
|
|
375
441
|
}
|
|
376
|
-
|
|
442
|
+
// 같은 복도라도 세로(가로) 구간이 안 겹치는 선은 같은 x(y) 에서 꺾어도 된다.
|
|
443
|
+
// 위로 가는 선과 아래로 가는 선을 괜히 벌리면 꺾이는 자리가 제각각이 된다.
|
|
444
|
+
// 겹치는 선끼리만 서로 다른 차선을 받는다 (그리디 색칠).
|
|
445
|
+
var midGroups = Object.create(null);
|
|
446
|
+
for (var mg = 0; mg < auto.length; mg++) {
|
|
447
|
+
if (auto[mg].mk) (midGroups[auto[mg].mk] = midGroups[auto[mg].mk] || []).push(auto[mg]);
|
|
448
|
+
}
|
|
449
|
+
for (var gk in midGroups) {
|
|
450
|
+
var grp = midGroups[gk];
|
|
451
|
+
var horiz = gk.charAt(0) === 'h';
|
|
452
|
+
var colors = 1;
|
|
453
|
+
for (var gi = 0; gi < grp.length; gi++) {
|
|
454
|
+
var ii = grp[gi];
|
|
455
|
+
var ax = horiz ? 1 : 0;
|
|
456
|
+
var lo3 = Math.min(ii.pa[ax], ii.pb[ax]), hi3 = Math.max(ii.pa[ax], ii.pb[ax]);
|
|
457
|
+
var used = [];
|
|
458
|
+
for (var gj = 0; gj < gi; gj++) {
|
|
459
|
+
var jj = grp[gj];
|
|
460
|
+
var lo4 = Math.min(jj.pa[ax], jj.pb[ax]), hi4 = Math.max(jj.pa[ax], jj.pb[ax]);
|
|
461
|
+
if (lo3 <= hi4 + 6 && lo4 <= hi3 + 6) used.push(jj.midColor);
|
|
462
|
+
}
|
|
463
|
+
var col = 0;
|
|
464
|
+
while (used.indexOf(col) >= 0) col++;
|
|
465
|
+
ii.midColor = col;
|
|
466
|
+
if (col + 1 > colors) colors = col + 1;
|
|
467
|
+
}
|
|
468
|
+
for (var gc = 0; gc < grp.length; gc++) grp[gc].colors = colors;
|
|
469
|
+
}
|
|
377
470
|
for (var m2 = 0; m2 < auto.length; m2++) {
|
|
378
471
|
var im = auto[m2];
|
|
379
472
|
var off = 0;
|
|
380
|
-
if (im.mk)
|
|
381
|
-
|
|
382
|
-
off = laneOffset(midSeen[im.mk], midCount[im.mk], 200);
|
|
383
|
-
}
|
|
384
|
-
im.pts = route(im.pa, im.a.s, im.pb, im.b.s, off);
|
|
473
|
+
if (im.mk) off = laneOffset(im.midColor, im.colors, 200);
|
|
474
|
+
im.pts = im.straight ? [im.pa, im.pb] : route(im.pa, im.a.s, im.pb, im.b.s, off);
|
|
385
475
|
}
|
|
386
476
|
|
|
387
477
|
var badRefs = [];
|