| name | service-icon |
| description | LinKHU에 새 서비스 아이콘을 만들 때 사용. 기존 아이콘의 실측된 도안 규칙(70x70, 선 두께, 색, 여백)에 맞춰 SVG로 그린 뒤 라이트·다크 두 벌 PNG로 내보내고 검증한다. "아이콘 만들어줘", "서비스 추가하고 아이콘 그려줘", "아이콘 스타일 맞춰줘" 같은 요청에 사용. |
서비스 아이콘 만들기
새 서비스를 src/data.js에 추가할 때 아이콘도 함께 만든다. 아이콘은 라이트·다크 두 벌이 필요하다.
기존 자산과 어긋나면 격자 안에서 혼자 튄다. 아래 도안 규칙은 2026-08-27 대량 추가 직전의 기존 117개를 실측해 뽑은 값이므로 추정으로 바꾸지 말고 그대로 따른다.
실측된 도안 규칙
| 항목 | 값 | 비고 |
|---|
| 캔버스 | 70×70 px | 실측 당시 117개 전부 동일. 예외 없음 |
| 배경 | 투명 | 두 벌 모두 |
| 여백 | 0~4 px | 중앙값 1 px. 도안이 캔버스를 거의 채운다 |
| 내용 면적 | 캔버스의 22~50% | 중앙값 34% |
| 선 두께 | 3 px 내외 | 70 px 래스터 기준 중앙값 3, 사분위 2~3 |
| 붉은색 비중 | 내용의 8~43% | 중앙값 23%. 강조로 쓰고 전면에 깔지 않는다 |
선 두께 3 px은 70 px 캔버스의 약 4.3%다. 팝업의 40 px에서는 약 1.7 px, 설정의 24 px에서는 약 1 px로 보인다. 기존 단과대·학과 아이콘이 이 굵기를 사용하므로 새 아이콘도 이를 기준으로 삼는다. 2 px보다 얇은 보조선은 24 px에서 사라질 수 있으므로 핵심 실루엣에 쓰지 않는다.
색
SVG에 적는 값은 라이트 벌 기준 하나뿐이다. 다크 벌은 생성기가 만든다.
팔레트는 세 가지뿐이다 — 브랜드 레드, 검정, 투명. 값의 원본은 spec/3-2-DESIGN-UI-RULES.md §3-2-4다.
| 용도 | SVG에 적는 값 | 다크 벌 |
|---|
| 선화·본체 | 검정 | 생성기가 무채색으로 대체 |
| 강조 | 브랜드 레드 | 생성기가 붉은색으로 대체 |
| 그 밖 | 투명 | 투명 |
두 열의 실제 hex는 모두 스펙 §3-2-4에 있다.
- 값을 여기에 적지 않는다. 스펙이 원본이고 값이 바뀐 이력이 있다. 두 곳에 적으면 갈라진다(이슈 #106이 그 사례다).
- 회색을 쓰지 않는다 (MUST). 회색은 팔레트에 없다.
- 회색·검정으로 넓은 면을 채우지 말고 선화로 그린다 (MUST). 예전 아이콘들은 면을 낮은 불투명도의 무채색으로 채웠는데, 흰 배경에서 회색으로 합성되어 카테고리마다 톤이 어긋났다. 전량 정합했고 경과는 스펙 §3-3-17에 있다. 작은 빨간 강조 면은 허용하지만 전체 실루엣을 빨강으로 채우지 않는다.
- 경계 안티에일리어싱은 검정의 알파로만 표현한다. 이 픽셀이 흰 배경에서 회색으로 보이는 것은 정상이며, 회색 금지는 면에 대한 규칙이다.
- 다크 두 값도 스펙 §3-2-4가 원본이다. SVG에 다크 색을 적지 말고
npm run generate:dark-icons에 맡긴다.
- 다크 두 색은 다크 카드 위에서 비텍스트 기준 3:1을 넘는다. 실측 대비는 스펙 §3-2-4에 있다.
빨강을 왜 두 값으로 나누는가 — 진한 라이트용 빨강은 다크 배경에서 2점대로 묻히고, 밝은 다크용 빨강은 라이트 배경에서 대비가 얕다. 한 값으로 양쪽을 만족시키려면 중간 톤이 되어 라이트가 원래 대비를 크게 잃는다. 실패 경과는 spec/3-3-DESIGN-DECISIONS.md §3-3-16에 있다.
⚠️ 저장된 PNG의 픽셀값은 SVG에 적은 값과 다르다
생성기가 흰 배경을 걷어내며 알파를 역산하므로, 저장된 라이트 PNG에는 언프리멀티플라이된 값이 들어간다.
현재 값을 예로 들면 이렇다(값 자체는 스펙이 원본이다).
SVG에 #a40f16 → 저장된 PNG는 #9e0007 알파 240 → 흰 카드 위에서 다시 #a40f16
PNG를 grep해서 브랜드 레드가 있는지 확인하려 하지 마라. 0개로 나온다. 확인하려면 흰 배경과 합성한 값을 계산한다.
만드는 순서
1. SVG로 그린다
<svg xmlns="http://www.w3.org/2000/svg" width="70" height="70" viewBox="0 0 70 70">
<g fill="none" stroke="<스펙 §3-2-4의 검정>" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
</g>
<path d="..." fill="<스펙 §3-2-4의 브랜드 레드>"/>
</svg>
원본은 design/icons/<카테고리>/<id>.svg에 남긴다. 색 규칙이 바뀌면 다시 내보내면 된다.
src/ 안에 두지 않는다. scripts/package-extension.js는 src/ 아래 모든 파일을 확장 패키지에 넣으므로, SVG가 그 안에 있으면 확장에 함께 배포된다.
2. 70×70 PNG로 내보낸다
의존성을 추가하지 않고 macOS 기본 도구만 쓴다.
qlmanage -s 는 SVG를 확대하지 않는다. SVG를 고유 크기(width/height)로 그린 뒤 캔버스만 키우므로, -s 70으로 뽑으면 작은 그림이 큰 여백 안에 놓인다. width/height를 키운 사본을 만들어야 한다.
ID=aikhu; CAT=common
SVG=design/icons/$CAT/$ID.svg
sed 's/width="70" height="70"/width="280" height="280"/' "$SVG" > /tmp/$ID-280.svg
qlmanage -t -s 280 -o /tmp /tmp/$ID-280.svg
sips -z 70 70 "/tmp/$ID-280.svg.png" --out "src/images/$CAT/$ID.png"
280으로 뽑아 70으로 줄이는 이유는 경계 안티에일리어싱을 얻기 위해서다. 70에서 바로 뽑으면 선이 거칠다.
내보낸 결과는 흰 배경 불투명 PNG다. 다음 단계에서 생성기가 걷어낸다.
헤드리스 Chrome(--screenshot)으로도 되지만 이 저장소에서 검증하지 않았다. 위 절차를 먼저 쓴다.
3. 두 벌을 만든다
npm run generate:dark-icons
- 라이트 원본의 흰 배경을 걷어내고
src/images/dark/<카테고리>/<id>.png에 다크 벌을 생성한다
다크 아이콘을 손으로 그리거나 고치지 않는다. 다음 실행에 덮인다.
<카테고리>는 common / colleges / departments 중 하나다. 이 폴더 이름은 파일을 나눠 담는 관례일 뿐 데이터의 category 값과 대응하지 않는다 (스펙 §4-2). 단과대는 colleges, 학과는 departments, 나머지 주제 카테고리는 전부 common에 둔다.
4. 데이터에 등록한다
src/data.js의 MASTER_SITE_LIST에 추가한다. imgSrc는 라이트 경로만 적는다 — 다크 경로는 코드가 규칙으로 파생한다.
{ id: "aikhu", name: "AiKHU", url: "https://dx.khu.ac.kr/aimentoring.html",
imgSrc: "images/common/aikhu.png", category: "교육·역량" }
규칙은 spec/4-DATA.md를 따른다.
5. 검증한다
npm run generate:landing-data
npm run build
위의 도안 규칙을 눈대중으로 맞추지 말고 실측한다. 표의 외곽·잉크·붉은 비중은 재보지 않으면 맞는지 알 수 없다. 눈으로는 멀쩡해 보이던 도안이 세로 46 px로 나온 적이 있다(이슈 #186).
python3 - <<'EOF'
from PIL import Image
def stat(p):
im = Image.open(p).convert("RGBA"); w, h = im.size; px = im.load()
xs = []; ys = []; ink = 0; red = 0
for y in range(h):
for x in range(w):
r, g, b, a = px[x, y]
if a > 20:
xs.append(x); ys.append(y); ink += 1
if r > 110 and g < 90 and b < 90: red += 1
return max(xs)-min(xs)+1, max(ys)-min(ys)+1, ink*100.0/(w*h), red*100.0/ink
print("목표 외곽 58~66 잉크 22~35% 붉은 15~25%")
for name in ["<id>"]:
bw, bh, ink, red = stat(f"src/images/common/{name}.png")
print(f"{name} {bw}x{bh} {ink:.1f}% {red:.1f}%")
EOF
⚠️ 3단계(generate:dark-icons)를 돌린 뒤에 재라. 2단계에서 나온 PNG는 흰 배경이 불투명하다. 그대로 재면 잉크가 100%, 외곽이 70×70으로 나와 아무것도 알 수 없다.
그리고 눈으로 확인한다. 계산만으로는 안 보이는 문제가 있다.
chrome://extensions에서 src/를 로드해 팝업(40 px)과 설정(24 px)에서 본다.
- 테마를 바꿔 두 벌 모두 확인한다. 다크에서 안 보이는 아이콘이 가장 흔한 실패다.
- 24 px에서 형태가 뭉개지지 않는지 본다. 세부가 많으면 단순화한다.
도안 원칙
- 주 상징 하나 + 보조 단서 1~2개. 기존 단과대·학과 아이콘은 학문·공간·도구 같은 단서를 작은 삽화처럼 조합한다. 주 상징은 3 px로 먼저 읽히게 하고 보조 단서는 2 px 내외로 둔다. 글자나 서비스와 무관한 장식은 넣지 않는다.
- 캔버스를 충분히 채운다. 기존 단과대·학과 94개의 내용 경계 중앙값은 65×63 px, 잉크 점유율은 28.7%다. 새 도안은 외곽이 대체로 58
66 px 안에 들고 잉크 점유율 2235%를 목표로 한다. 작은 기호 하나를 중앙에 놓아 넓게 비우지 않는다.
- 강조는 한두 곳에. 붉은색을 도안 전체에 쓰지 않는다. 기존 단과대·학과의 붉은 픽셀 비중 중앙값은 내용의 22.8%다. 대체로 15~25% 안에서 선이나 작은 면으로 쓴다.
- 같은 계열 서비스는 같은 상징 계열로. 단과대는 건물·학문 상징, 교내 주제별 서비스는 기능 상징을 쓴다.
- 대학 로고·휘장을 쓰지 않는다. LinKHU는 경희대학교 공식 산출물이 아니다 (스펙 §3-2-1).
- 서비스가 다르면 아이콘도 다르게 만든다. 여러 서비스가 한 아이콘을 공유하면 팝업 격자에서 구분이 안 된다.
- 같은 계열 두 개를 한 번에 만들 때는 주 상징 자체를 가른다 (MUST). 계열을 맞추려다 같은 실루엣에 안쪽 기호만 바꾸면 40 px에서 두 칸이 같아 보인다. 계열감은 선 두께와 밀도가 이미 만들어 주므로 실루엣까지 같을 필요가 없다. 경희사이버대 포털·LMS를 처음에 같은 노트북으로 그렸다가 다시 그린 경과가 이슈 #186에 있다.
예시
| 서비스 | 주 상징 (3 px) | 보조 단서 (2 px) | 강조 | 실측 (외곽 / 잉크 / 붉은) |
|---|
| AiKHU | 말풍선 | 좌우 노드와 연결선 | 붉은 4각 스파클 | — |
| AI Hub | 중심 노드 | 위성 노드 3개 연결 | 붉은 중심 | — |
| 경희사이버대 포털 | 아치형 관문 | 안쪽 아치, 좌우 가로선 | 붉은 열쇠구멍 | 59×56 / 24.3% / 17.3% |
| 경희사이버대 LMS | 화면과 받침 | 와이파이 호 2개, 다리 | 붉은 재생 버튼 | 59×52 / 22.4% / 16.3% |
선 굵기만 따르지 말고 삽화 밀도와 캔버스 점유 기준을 5단계에서 실제로 재서 확인한다.
하지 말 것
filter: invert()로 다크를 만들지 않는다. 명도를 뒤집으면 진한 빨강이 핑크가 되고, 필터는 요소 전체에 걸려 선화만 골라 처리할 수 없다.
- 아이콘 뒤에 밝은 타일을 깔지 않는다. 다크 화면에서 흰 상자가 격자마다 박혀 눈부시다.
- 한 벌로 두 테마를 겸하지 않는다. 양쪽 하한에 끼어 중간 톤만 남고 라이트가 대비를 잃는다.
세 방식 모두 시도했다가 폐기했다. 경과와 실측은 스펙 §3-3-16에 있다.