feat: 한국어 우선 쇼케이스 정리
All checks were successful
ci / build (push) Successful in 51s
release / release (push) Successful in 2m34s

This commit is contained in:
Yun Chan 2026-08-26 18:14:41 +09:00
parent e4087c29f5
commit 2b7fa32b5b
109 changed files with 437 additions and 279 deletions

View file

@ -616,7 +616,7 @@ Base 레이아웃에서 홈 판별 후 `/#…` 로 접두사(언어판별). 홈
과제·성적) 사이드 전환 + aria-current 이동, 검색·상태칩 필터, 학생 상세 모달 과제·성적) 사이드 전환 + aria-current 이동, 검색·상태칩 필터, 학생 상세 모달
(메모 추가 폼), 채점 모달(저장 시 표 상태 '채점 대기→채점 완료' 갱신). 모달은 (메모 추가 폼), 채점 모달(저장 시 표 상태 '채점 대기→채점 완료' 갱신). 모달은
네이티브 <dialog> — 쇼케이스 모달과 같은 결정론(Esc·포커스 트랩을 브라우저에). 네이티브 <dialog> — 쇼케이스 모달과 같은 결정론(Esc·포커스 트랩을 브라우저에).
- **dure-enrollment 두레대학 수강신청**: 남색 잉크 + 주홍 申 도장. 과목 10개, - **dure-enrollment 두레대학 수강신청**: 남색 잉크 + 주홍 신청 도장. 과목 10개,
시간표 블록 grid-row=(start−9)×2+2(헤더 1행 오프셋), 겹침·정원 마감·학점 18 시간표 블록 grid-row=(start−9)×2+2(헤더 1행 오프셋), 겹침·정원 마감·학점 18
초과를 도장 누르기 전에 차단(openCourse 에서 warn + apply disabled), 결과는 초과를 도장 누르기 전에 차단(openCourse 에서 warn + apply disabled), 결과는
인라인 status(aria-live)로. 취소는 블록 제거·학점 회수까지 되돌림. 인라인 status(aria-live)로. 취소는 블록 제거·학점 회수까지 되돌림.
@ -674,7 +674,7 @@ Base 레이아웃에서 홈 판별 후 `/#…` 로 접두사(언어판별). 홈
translateY(14px)+scale(0.985) 240ms, 닫기 버튼 hover 채움(흑연/남색). translateY(14px)+scale(0.985) 240ms, 닫기 버튼 hover 채움(흑연/남색).
- 검증: 프로덕션 margin 상하 261.656/261.672 대칭(완전 중앙), -2 컷 재캡처. - 검증: 프로덕션 margin 상하 261.656/261.672 대칭(완전 중앙), -2 컷 재캡처.
`2026-08-22` · 국소 — 모달 재구성(사용자 지시: 빈약하고 안 이쁘다). "작은 상자에 `2026-08-22` · 국소 — 모달 재구성(사용자 지시: 빈약하고 안 이쁘다). "작은 상자에
항목 몇 개"를 넘어 모달을 판(版)으로: 가온 학생 모달 760px 학적 카드(큰 숫자 3개, 항목 몇 개"를 넘어 모달을 판으로: 가온 학생 모달 760px 학적 카드(큰 숫자 3개,
**실제 ATT 데이터에서 뽑은 2주 출석 스트립 10칸**, 날짜열 상담 타임라인), 채점 모달 **실제 ATT 데이터에서 뽑은 2주 출석 스트립 10칸**, 날짜열 상담 타임라인), 채점 모달
평균 실시간 계산(91.7→84.3, 형광펜 강조), 두레 강의계획 720px(큰 숫자 3개, **주간 평균 실시간 계산(91.7→84.3, 형광펜 강조), 두레 강의계획 720px(큰 숫자 3개, **주간
자리 미니 격자** 9–18시 비례 레일, 평가 비중 **누적 막대 한 줄** — 같은 남색의 농도 자리 미니 격자** 9–18시 비례 레일, 평가 비중 **누적 막대 한 줄** — 같은 남색의 농도
@ -796,7 +796,7 @@ max-age=0 지정(존 설정이 우선하면 효과는 제한적). **운칙: /wor
디자인 스킬 13종 전량 병행 — designpaca 연장 경로(토큰·컨셉 승계) + redesign-existing-projects 감사 우선. taste 계열(안티슬롭·하이엔드·GSAP·미니멀·브루탈리스트·스티치)은 관리 콘솔에 적용되는 규칙만(상태 완결·대비·모션 문법·카드 절제·메타라벨 금지), 이미지젠 계열 4종은 구성·계층·가독성 기준으로 감사에 반영(관리 콘솔에 사진 소재는 부적합 — 생성 도구도 이 하니스에 없음). 디자인 스킬 13종 전량 병행 — designpaca 연장 경로(토큰·컨셉 승계) + redesign-existing-projects 감사 우선. taste 계열(안티슬롭·하이엔드·GSAP·미니멀·브루탈리스트·스티치)은 관리 콘솔에 적용되는 규칙만(상태 완결·대비·모션 문법·카드 절제·메타라벨 금지), 이미지젠 계열 4종은 구성·계층·가독성 기준으로 감사에 반영(관리 콘솔에 사진 소재는 부적합 — 생성 도구도 이 하니스에 없음).
- **브랜드 낡은 값 교정**: theme-color 가 종이 시대 값(#F7F5EF/#FBFAF7), 파비콘이 v7 프리셋 전환 이전 문양(형광펜/남색)이었다. #fafafa + 현 브랜드 마크(가온=에메랄드 '가', 두레=벽돌 申)로 교체, og.png 재캡처. - **브랜드 낡은 값 교정**: theme-color 가 종이 시대 값(#F7F5EF/#FBFAF7), 파비콘이 v7 프리셋 전환 이전 문양(형광펜/남색)이었다. #fafafa + 현 브랜드 마크(가온=에메랄드 '가', 두레=벽돌색 신청)로 교체, og.png 재캡처.
- **상태 완결성**: 가온 채점 입력에 범위 검증(GAON.validateScore — 0–100 밖이면 인라인 경고 + 저장 차단 + aria-invalid, 평균은 유효값만 계산). 두레 취소에 실행취소(상태줄 안에 되돌리기 버튼 — 재신청은 canEnroll 로 재심사, 사이에 다른 신청이 끼면 정당하게 막힘). 알림 센터 Esc 닫힘. 로딩 상태는 의도적으로 만들지 않았다 — 동기 인라인 데이터에 스켈레톤은 '저장을 흉내내지 않는다' 원칙 위반. - **상태 완결성**: 가온 채점 입력에 범위 검증(GAON.validateScore — 0–100 밖이면 인라인 경고 + 저장 차단 + aria-invalid, 평균은 유효값만 계산). 두레 취소에 실행취소(상태줄 안에 되돌리기 버튼 — 재신청은 canEnroll 로 재심사, 사이에 다른 신청이 끼면 정당하게 막힘). 알림 센터 Esc 닫힘. 로딩 상태는 의도적으로 만들지 않았다 — 동기 인라인 데이터에 스켈레톤은 '저장을 흉내내지 않는다' 원칙 위반.
- **도메인 기능 — 인쇄**: @media print 2종. 가온은 활성 뷰를 서류로(모달이 열려 있으면 body:has(dialog[open]) 로 앱을 숨기고 그 서류만), 두레는 A4 가로 시간표 + 강의계획서. print-color-adjust: exact 로 상태색 보존. 탑바/앱바에 인쇄 버튼. - **도메인 기능 — 인쇄**: @media print 2종. 가온은 활성 뷰를 서류로(모달이 열려 있으면 body:has(dialog[open]) 로 앱을 숨기고 그 서류만), 두레는 A4 가로 시간표 + 강의계획서. print-color-adjust: exact 로 상태색 보존. 탑바/앱바에 인쇄 버튼.
- **키보드 단축키**: 숫자키 뷰 전환(가온 1–9, 두레 1–6), / 검색 포커스(입력 요소 가드). kbd 물리 키 칩 안내 — 가온 사이드 하단, 두레 앱바(모바일 숨김). - **키보드 단축키**: 숫자키 뷰 전환(가온 1–9, 두레 1–6), / 검색 포커스(입력 요소 가드). kbd 물리 키 칩 안내 — 가온 사이드 하단, 두레 앱바(모바일 숨김).
@ -848,7 +848,7 @@ v23 스크롤 리셋·단일 헤더에도 사용자 평가가 "여전히 깨져
사용자 스크린샷 지적 "h2(2026학년도 1학기 과목)가 화면 왼쪽에 너무 붙는다". 스크린샷을 픽셀 판정으로 해부해 확정: 앱바 텍스트·검색·칩·카드는 전부 ~24px에서 시작하는데 **h2만 16px** — 8px 왼쪽으로 어긋난 큰 명조 제목이 '가장자리에 붙어 깨졌다'로 읽혔다. 원인: `.layout` 모바일 인셋이 var(--space-4)=16px인데 앱바는 space-5=24px — 같은 화면에 두 축이 공존. 사용자 스크린샷 지적 "h2(2026학년도 1학기 과목)가 화면 왼쪽에 너무 붙는다". 스크린샷을 픽셀 판정으로 해부해 확정: 앱바 텍스트·검색·칩·카드는 전부 ~24px에서 시작하는데 **h2만 16px** — 8px 왼쪽으로 어긋난 큰 명조 제목이 '가장자리에 붙어 깨졌다'로 읽혔다. 원인: `.layout` 모바일 인셋이 var(--space-4)=16px인데 앱바는 space-5=24px — 같은 화면에 두 축이 공존.
- 수리: `.layout` 모바일 인셋 inline을 24px로 — 제목·카드·앱바가 같은 축. - 수리: `.layout` 모바일 인셋 inline을 24px로 — 제목·카드·앱바가 같은 축.
- **게이트 공백 발견**: 기존 매트릭스는 오른쪽 '오버플로'만 검사하고 왼쪽 인셋의 최소값·정렬 정합은 검사하지 않았다(그래서 기준 갱신에 결함이 그대로 박혔다). e2e에 **M6 왼쪽 인셋** 스위트 신설(60단언): 전 뷰×320/390에서 텍스트 시작점 ≥12px + 헤더-제목 같은 축(±10px). RED 38 실패로 결함 재현 확인 후 수리로 GREEN. - **게이트 공백 발견**: 기존 매트릭스는 오른쪽 '오버플로'만 검사하고 왼쪽 인셋의 최소값·정렬 정합은 검사하지 않았다(그래서 기준 갱신에 결함이 그대로 박혔다). e2e에 **M6 왼쪽 인셋** 스위트 신설(60단언): 전 뷰×320/390에서 텍스트 시작점 ≥12px + 헤더-제목 같은 축(±10px). RED 38 실패로 결함 재현 확인 후 수리로 GREEN.
- 측정 하자(瑕) 2건 교정: display:none 요소의 rect=0이 잡히던 것(vis 필터), 전폭 컨테이너 보더박스가 아닌 '텍스트 시작점' 기준. - 측정 하자 2건 교정: display:none 요소의 rect=0이 잡히던 것(vis 필터), 전폭 컨테이너 보더박스가 아닌 '텍스트 시작점' 기준.
- 시각 기준 갱신(두레 390px 6화면), verify L0~L6 전 통과(E2E 1,805단언). - 시각 기준 갱신(두레 390px 6화면), verify L0~L6 전 통과(E2E 1,805단언).
## 2026-08-23 — 신규 쇼케이스 3종 구축 (청온 케어 · 캐스크 아카이브 · 마테리아 스튜디오) ## 2026-08-23 — 신규 쇼케이스 3종 구축 (청온 케어 · 캐스크 아카이브 · 마테리아 스튜디오)
@ -1133,3 +1133,5 @@ Pretendard `dynamic-subset` 은 유니코드 범위별로 파일을 쪼개고
- 최초 실행에서 CSS 토큰 경로 404와 응답하지 않는 클립보드 권한을 각각 재현했다. 공개 검증에서는 캐시된 이전 RFS 스크립트도 재현해 외부 스크립트 URL에 버전을 붙였다. 토큰을 명시적 스타일 링크로 바꾸고 복사 폴백을 추가한 뒤 성능 계측을 포함해 **115/115 통과**했다. - 최초 실행에서 CSS 토큰 경로 404와 응답하지 않는 클립보드 권한을 각각 재현했다. 공개 검증에서는 캐시된 이전 RFS 스크립트도 재현해 외부 스크립트 URL에 버전을 붙였다. 토큰을 명시적 스타일 링크로 바꾸고 복사 폴백을 추가한 뒤 성능 계측을 포함해 **115/115 통과**했다.
- `showcase-audit.mjs rfs-cfd saebyeok-cheonggwa`는 두 페이지 × 320/390/768/1440, 총 **8/8 통과**했다. - `showcase-audit.mjs rfs-cfd saebyeok-cheonggwa`는 두 페이지 × 320/390/768/1440, 총 **8/8 통과**했다.
- 위 전용 E2E는 저장소 전체 `verify.mjs`의 L6에 포함한다. 배포 전에는 전체 게이트를 다시 실행한다. - 위 전용 E2E는 저장소 전체 `verify.mjs`의 L6에 포함한다. 배포 전에는 전체 게이트를 다시 실행한다.
- 2026-08-26 국소 조정: 배포 텍스트에서 한자를 전부 차단하고, 쇼케이스의 세로 조판·한지/먹·인장/도장·다도 연출은 가로 라벨, 중립 상태 배지, 금속 활자·편집 도구, 독서·정밀 목공 언어로 교체했다. 한국어 고유명사와 역사 소재는 유지하되 동아시아 전통 장식을 제품의 시각 단축어로 쓰지 않는다.

View file

@ -1,4 +1,4 @@
# 게이트 리포트 — 2026-08-24 19:46 # 게이트 리포트 — 2026-08-26 09:11
**전 항목 통과 — 배포 가능** **전 항목 통과 — 배포 가능**

View file

@ -12,7 +12,9 @@
"test:e2e:twentyoz": "node tools/twentyoz-e2e.mjs", "test:e2e:twentyoz": "node tools/twentyoz-e2e.mjs",
"prebuild": "npm run build:showcase-assets", "prebuild": "npm run build:showcase-assets",
"build": "astro build", "build": "astro build",
"postbuild": "node tools/no-hanja.mjs --root dist",
"preview": "astro preview", "preview": "astro preview",
"test:no-hanja": "node tools/no-hanja.mjs",
"typecheck": "echo \"(사이트는 build 로 검증한다)\"", "typecheck": "echo \"(사이트는 build 로 검증한다)\"",
"verify": "node tools/verify.mjs" "verify": "node tools/verify.mjs"
}, },

View file

@ -75,7 +75,7 @@
천 년의 비색은<br />말이 없다 천 년의 비색은<br />말이 없다
</h1> </h1>
<p class="lead reveal" style="--delay: 240ms"> <p class="lead reveal" style="--delay: 240ms">
비색(翡色)은 말로 설명되는 색이 아닙니다. 1,280°C의 환원 불에서만 비색은 말로 설명되는 색이 아닙니다. 1,280°C의 환원 불에서만
나오는, 푸르름과 푸름이 아닌 사이의 색. 이 전시는 그 색을 설명하지 나오는, 푸르름과 푸름이 아닌 사이의 색. 이 전시는 그 색을 설명하지
않고 세 개의 암실에 놓아 두었습니다. 않고 세 개의 암실에 놓아 두었습니다.
</p> </p>

View file

@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>1:1 위스키 도슨트 테이스팅 마스터클래스 — CASK ARCHIVE (樽室)</title> <title>1:1 위스키 도슨트 테이스팅 마스터클래스 — CASK ARCHIVE</title>
<meta name="description" content="캐스크 아카이브의 1:1 위스키 도슨트 테이스팅 마스터클래스 안내. 코피타 글래스웨어학, 감각 아로마 휠 분석, 독립병입자 싱글 캐스크 플라이트 및 가스트로노미 페어링." /> <meta name="description" content="캐스크 아카이브의 1:1 위스키 도슨트 테이스팅 마스터클래스 안내. 코피타 글래스웨어학, 감각 아로마 휠 분석, 독립병입자 싱글 캐스크 플라이트 및 가스트로노미 페어링." />
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin /> <link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
@ -140,7 +140,6 @@
<header class="site-head"> <header class="site-head">
<div class="wrap head-inner"> <div class="wrap head-inner">
<a href="../index.html" class="wordmark" aria-label="캐스크 아카이브 홈"> <a href="../index.html" class="wordmark" aria-label="캐스크 아카이브 홈">
<span class="wordmark-han" style="font-size: var(--step-0); color: var(--accent);">樽</span>
캐스크 아카이브 <span class="wordmark-han">CASK ARCHIVE</span> 캐스크 아카이브 <span class="wordmark-han">CASK ARCHIVE</span>
</a> </a>
<nav class="nav" aria-label="메인 내비게이션"> <nav class="nav" aria-label="메인 내비게이션">

View file

@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>캐스크 아카이브 (CASK ARCHIVE · 樽室) · 독립병입자 위스키바 & 도슨트 큐레이션</title> <title>캐스크 아카이브 (CASK ARCHIVE) · 독립병입자 위스키바 & 도슨트 큐레이션</title>
<meta <meta
name="description" name="description"
content="Gordon & MacPhail, Signatory, SMWS 등 독립병입자(IB) 희귀 캐스크 공식 수입 인증 위스키바. 캐스크 넘버 원장과 1:1 위스키 도슨트 테이스팅 코스 예약." content="Gordon & MacPhail, Signatory, SMWS 등 독립병입자(IB) 희귀 캐스크 공식 수입 인증 위스키바. 캐스크 넘버 원장과 1:1 위스키 도슨트 테이스팅 코스 예약."
@ -42,7 +42,7 @@
<header class="site-head"> <header class="site-head">
<div class="wrap head-inner"> <div class="wrap head-inner">
<a class="wordmark" href="./index.html"> <a class="wordmark" href="./index.html">
樽 캐스크 아카이브 <span class="wordmark-han">CASK ARCHIVE</span> 캐스크 아카이브 <span class="wordmark-han">CASK ARCHIVE</span>
</a> </a>
<nav class="nav" aria-label="메인 메뉴"> <nav class="nav" aria-label="메인 메뉴">
@ -124,7 +124,7 @@
</div> </div>
<div class="cert-item"> <div class="cert-item">
<div class="cert-name">CADENHEAD'S</div> <div class="cert-name">CADENHEAD'S</div>
<div class="cert-desc">1842년 스코틀랜드 최고(最古) 독립병입자 라인업</div> <div class="cert-desc">1842년 설립, 스코틀랜드에서 가장 오래된 독립병입자 라인업</div>
</div> </div>
</div> </div>
</section> </section>

View file

@ -1,7 +1,7 @@
@import "./tokens.css"; @import "./tokens.css";
/* ========================================================================== /* ==========================================================================
캐스크 아카이브 (CASK ARCHIVE · 樽室) — 메인 스타일시트 SSOT 캐스크 아카이브 (CASK ARCHIVE) — 메인 스타일시트 SSOT
프리셋: editorial-archival + dark-instrument + high-end-visual-design 프리셋: editorial-archival + dark-instrument + high-end-visual-design
========================================================================== */ ========================================================================== */

View file

@ -1,5 +1,5 @@
/* ========================================================================== /* ==========================================================================
캐스크 아카이브 (CASK ARCHIVE · 樽室) — 디자인 토큰 SSOT 캐스크 아카이브 (CASK ARCHIVE) — 디자인 토큰 SSOT
프리셋: dark-instrument + editorial-archival 프리셋: dark-instrument + editorial-archival
컨셉: 독립병입자 희귀 캐스크 원장과 1:1 위스키 도슨트 — 오크와 피트의 시간 컨셉: 독립병입자 희귀 캐스크 원장과 1:1 위스키 도슨트 — 오크와 피트의 시간
단일 강조색: 앰버 골드 (#D99B38) 단일 강조색: 앰버 골드 (#D99B38)

View file

@ -3,12 +3,12 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>청온 케어 (淸溫) · 프리미엄 주거·오피스 전문 위생 케어 & 관제 콘솔</title> <title>청온 케어 · 프리미엄 주거·오피스 전문 위생 케어 & 관제 콘솔</title>
<meta <meta
name="description" name="description"
content="임상 수준의 6단계 위생 프로토콜과 100% 정찰제 견적. 독일 카처 4.0bar 고온스팀과 에코서트 인증 천연 세제. 실시간 견적 신청 및 관제 콘솔 지원." content="임상 수준의 6단계 위생 프로토콜과 100% 정찰제 견적. 독일 카처 4.0bar 고온스팀과 에코서트 인증 천연 세제. 실시간 견적 신청 및 관제 콘솔 지원."
/> />
<meta property="og:title" content="청온 케어 (淸溫) · 프리미엄 위생 케어" /> <meta property="og:title" content="청온 케어 · 프리미엄 위생 케어" />
<meta property="og:description" content="공간의 숨결을 되돌리는 6단계 프로토콜과 투명한 정찰제 견적." /> <meta property="og:description" content="공간의 숨결을 되돌리는 6단계 프로토콜과 투명한 정찰제 견적." />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta name="theme-color" content="#f8faf9" /> <meta name="theme-color" content="#f8faf9" />
@ -38,8 +38,8 @@
<header class="site-head"> <header class="site-head">
<div class="wrap head-inner"> <div class="wrap head-inner">
<a class="wordmark" href="#" id="brandLogo"> <a class="wordmark" href="#" id="brandLogo">
<span class="wordmark-badge">淸</span> <span class="wordmark-badge">청</span>
청온 케어 <span class="wordmark-han">淸溫</span> 청온 케어
</a> </a>
<nav class="nav" aria-label="메인 메뉴"> <nav class="nav" aria-label="메인 메뉴">

View file

@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>임상 6단계 위생 프로토콜 & 한남 케이스 리포트 — 청온 케어 (淸溫)</title> <title>임상 6단계 위생 프로토콜 & 한남 케이스 리포트 — 청온 케어</title>
<meta name="description" content="청온 케어의 병원급 6단계 정밀 위생 프로토콜 및 ATP 생체 오염도 측정 검증 리포트. 나인원한남 86평형 준공 청소 케이스 스터디 및 32개 정밀 QC 체크리스트." /> <meta name="description" content="청온 케어의 병원급 6단계 정밀 위생 프로토콜 및 ATP 생체 오염도 측정 검증 리포트. 나인원한남 86평형 준공 청소 케이스 스터디 및 32개 정밀 QC 체크리스트." />
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin /> <link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
@ -131,8 +131,8 @@
<header class="site-head"> <header class="site-head">
<div class="wrap head-inner"> <div class="wrap head-inner">
<a href="../index.html" class="wordmark" aria-label="청온 케어 홈"> <a href="../index.html" class="wordmark" aria-label="청온 케어 홈">
<span class="wordmark-badge">淸</span> <span class="wordmark-badge">청</span>
청온 케어 <span class="wordmark-han">淸溫</span> 청온 케어
</a> </a>
<nav class="nav" aria-label="메인 내비게이션"> <nav class="nav" aria-label="메인 내비게이션">
<a href="./index.html" class="active" style="color: var(--accent); font-weight: 600;">6단계 프로토콜</a> <a href="./index.html" class="active" style="color: var(--accent); font-weight: 600;">6단계 프로토콜</a>

View file

@ -1,7 +1,7 @@
@import "./tokens.css"; @import "./tokens.css";
/* ========================================================================== /* ==========================================================================
청온 케어 (CHEONG-ON CARE · 淸溫) — 메인 스타일시트 SSOT 청온 케어 (CHEONG-ON CARE) — 메인 스타일시트 SSOT
프리셋: clinical-sanitation + transparent-calculator + high-end-visual-design 프리셋: clinical-sanitation + transparent-calculator + high-end-visual-design
========================================================================== */ ========================================================================== */

View file

@ -1,5 +1,5 @@
/* ========================================================================== /* ==========================================================================
청온 케어 (淸溫 · CHEONG-ON CARE) — 디자인 토큰 SSOT 청온 케어 (CHEONG-ON CARE) — 디자인 토큰 SSOT
프리셋: ledger-console + swiss-minimal 프리셋: ledger-console + swiss-minimal
컨셉: 임상 수준의 위생 프로토콜과 정직한 정찰제 — 공간의 숨결을 되돌리는 케어 컨셉: 임상 수준의 위생 프로토콜과 정직한 정찰제 — 공간의 숨결을 되돌리는 케어
단일 강조색: 에메랄드 틸 (#0D9488) 단일 강조색: 에메랄드 틸 (#0D9488)

View file

@ -9,11 +9,11 @@
<link rel="manifest" href="./manifest.webmanifest" /> <link rel="manifest" href="./manifest.webmanifest" />
<link rel="apple-touch-icon" href="./icon-192.png" /> <link rel="apple-touch-icon" href="./icon-192.png" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:title" content="두레대학 수강신청 — 시간표 도장 앱" /> <meta property="og:title" content="두레대학 수강신청 — 시간표 관리 앱" />
<meta property="og:url" content="https://designpaca.chanpaca.net/work/dure-enrollment/" /> <meta property="og:url" content="https://designpaca.chanpaca.net/work/dure-enrollment/" />
<meta property="og:image" content="https://designpaca.chanpaca.net/work/dure-enrollment/og.png" /> <meta property="og:image" content="https://designpaca.chanpaca.net/work/dure-enrollment/og.png" />
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="두레대학 수강신청 — 시간표 도장 앱" /> <meta name="twitter:title" content="두레대학 수강신청 — 시간표 관리 앱" />
<meta name="twitter:image" content="https://designpaca.chanpaca.net/work/dure-enrollment/og.png" /> <meta name="twitter:image" content="https://designpaca.chanpaca.net/work/dure-enrollment/og.png" />
<meta <meta
name="description" name="description"
@ -49,7 +49,7 @@
<header class="appbar"> <header class="appbar">
<div class="appbar-inner"> <div class="appbar-inner">
<a class="brand" href="#main"> <a class="brand" href="#main">
<span class="brand-seal" aria-hidden="true">申</span> <span class="brand-mark" aria-hidden="true"></span>
두레대학 수강신청 두레대학 수강신청
</a> </a>
<div class="appbar-actions"> <div class="appbar-actions">
@ -108,7 +108,7 @@
<p class="tt-legend"> <p class="tt-legend">
<span class="key"><i class="k-major"></i>전공</span> <span class="key"><i class="k-major"></i>전공</span>
<span class="key"><i class="k-gen"></i>교양</span> <span class="key"><i class="k-gen"></i>교양</span>
<span class="key"><i class="k-seal"></i>신청됨</span> <span class="key"><i class="k-state"></i>신청됨</span>
</p> </p>
</div> </div>
</aside> </aside>
@ -230,8 +230,7 @@
<dialog id="course-dialog" class="modal modal-split" aria-labelledby="cd-h"> <dialog id="course-dialog" class="modal modal-split" aria-labelledby="cd-h">
<div class="mspread"> <div class="mspread">
<div class="mcover"> <div class="mcover">
<span class="mcover-mark" aria-hidden="true">申</span> <span class="mcover-label" aria-hidden="true">COURSE</span>
<span class="mcover-seal" aria-hidden="true">申</span>
<h2 class="mcover-name" id="cd-h">과목명</h2> <h2 class="mcover-name" id="cd-h">과목명</h2>
<p class="mcover-meta mono" id="cd-code">CODE · 계열</p> <p class="mcover-meta mono" id="cd-code">CODE · 계열</p>
<dl class="mcover-stats"> <dl class="mcover-stats">
@ -261,7 +260,7 @@
<p class="warn" id="cd-warn" hidden></p> <p class="warn" id="cd-warn" hidden></p>
<div class="modal-actions"> <div class="modal-actions">
<button type="button" class="btn-seal" id="cd-apply">수강 신청</button> <button type="button" class="btn-primary" id="cd-apply">수강 신청</button>
<button type="button" class="btn-line" id="cd-cancel" hidden>신청 취소</button> <button type="button" class="btn-line" id="cd-cancel" hidden>신청 취소</button>
<button type="button" class="btn-star" id="cd-star" aria-pressed="false">관심 남기기</button> <button type="button" class="btn-star" id="cd-star" aria-pressed="false">관심 남기기</button>
</div> </div>
@ -501,7 +500,7 @@
<span class="c-credit">${c.credits}학점</span> <span class="c-credit">${c.credits}학점</span>
<span class="c-seats ${c.seats <= 5 ? "is-low" : ""}">${c.seats === 0 ? "정원 마감" : "여석 " + c.seats}</span> <span class="c-seats ${c.seats <= 5 ? "is-low" : ""}">${c.seats === 0 ? "정원 마감" : "여석 " + c.seats}</span>
${starred.has(c.id) ? '<span class="c-star">★</span>' : ""} ${starred.has(c.id) ? '<span class="c-star">★</span>' : ""}
${enrolled.has(c.id) ? '<span class="c-seal">申</span>' : ""} ${enrolled.has(c.id) ? '<span class="c-enrolled">신청</span>' : ""}
</span> </span>
</button>`; </button>`;
return li; return li;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Before After
Before After

View file

@ -2,7 +2,7 @@
/* ========================================================================== /* ==========================================================================
두레대학 수강신청 — 프로덕션 콘솔 (shadcn/ui new-york 문법의 바닐라 이식) 두레대학 수강신청 — 프로덕션 콘솔 (shadcn/ui new-york 문법의 바닐라 이식)
액센트는 벽돌(申) 하나. 나머지는 무채상 완성도로 말한다. 액센트는 벽돌색 하나. 나머지는 무채상 완성도로 말한다.
========================================================================== */ ========================================================================== */
*, *,
@ -92,15 +92,13 @@ h3 { font-size: var(--step-0); font-weight: 500; }
font-family: var(--font-display); font-family: var(--font-display);
display: inline-flex; align-items: center; gap: var(--space-2); display: inline-flex; align-items: center; gap: var(--space-2);
color: var(--foreground); text-decoration: none; color: var(--foreground); text-decoration: none;
font-weight: 600; font-size: var(--step-1); font-weight: 600; font-size: var(--step-1); white-space: nowrap;
} }
.brand-seal { .brand-mark {
display: inline-flex; align-items: center; justify-content: center; display: inline-block;
width: 26px; height: 26px; width: 10px; height: 10px;
background: var(--accent); background: var(--accent);
color: var(--on-accent); border-radius: 2px;
border-radius: var(--radius);
font-size: var(--step-0); font-weight: 600;
} }
.credits { .credits {
display: inline-flex; align-items: center; gap: 6px; display: inline-flex; align-items: center; gap: 6px;
@ -251,12 +249,15 @@ h3 { font-size: var(--step-0); font-weight: 500; }
.c-seats { color: var(--muted-foreground); } .c-seats { color: var(--muted-foreground); }
.c-seats.is-low { color: var(--danger); font-weight: 600; } .c-seats.is-low { color: var(--danger); font-weight: 600; }
.c-star { color: var(--accent); font-weight: 600; } .c-star { color: var(--accent); font-weight: 600; }
.c-seal { .c-enrolled {
display: inline-grid; place-items: center; display: inline-flex; align-items: center;
width: 22px; height: 22px; color: var(--accent-ink);
background: var(--accent); color: var(--on-accent); background: var(--accent-soft);
border: 1px solid var(--accent);
border-radius: var(--radius-xs); border-radius: var(--radius-xs);
font-size: var(--step--1); padding: 1px 5px;
font-size: var(--step--2);
font-weight: 600;
} }
.empty { padding: var(--space-4) 0; color: var(--muted-foreground); } .empty { padding: var(--space-4) 0; color: var(--muted-foreground); }
@ -378,10 +379,10 @@ tbody tr:hover { background: var(--muted); }
.key i { width: 10px; height: 10px; display: inline-block; border-radius: var(--radius-xs); } .key i { width: 10px; height: 10px; display: inline-block; border-radius: var(--radius-xs); }
.k-major { background: var(--accent-soft); box-shadow: inset 2px 0 0 var(--accent); } .k-major { background: var(--accent-soft); box-shadow: inset 2px 0 0 var(--accent); }
.k-gen { background: var(--muted-2); box-shadow: inset 2px 0 0 var(--border-strong); } .k-gen { background: var(--muted-2); box-shadow: inset 2px 0 0 var(--border-strong); }
.k-seal { background: var(--accent); } .k-state { background: var(--accent); }
/* ── 버튼 ── */ /* ── 버튼 ── */
.btn-seal, .btn-line, .btn-star { .btn-primary, .btn-line, .btn-star {
appearance: none; cursor: pointer; font: inherit; appearance: none; cursor: pointer; font: inherit;
font-size: var(--step-0); font-weight: 500; font-size: var(--step-0); font-weight: 500;
height: 36px; padding: 0 var(--space-4); height: 36px; padding: 0 var(--space-4);
@ -390,17 +391,17 @@ tbody tr:hover { background: var(--muted); }
transition: background-color var(--dur-2) var(--ease-out), border-color var(--dur-2) var(--ease-out), color var(--dur-2) var(--ease-out), scale var(--dur-1) var(--ease-out); transition: background-color var(--dur-2) var(--ease-out), border-color var(--dur-2) var(--ease-out), color var(--dur-2) var(--ease-out), scale var(--dur-1) var(--ease-out);
white-space: nowrap; white-space: nowrap;
} }
.btn-seal { background: var(--accent); border: 1px solid var(--accent); color: var(--on-accent); box-shadow: var(--shadow-sm); } .btn-primary { background: var(--accent); border: 1px solid var(--accent); color: var(--on-accent); box-shadow: var(--shadow-sm); }
.btn-seal:hover:not(:disabled) { background: var(--accent-ink); border-color: var(--accent-ink); } .btn-primary:hover:not(:disabled) { background: var(--accent-ink); border-color: var(--accent-ink); }
.btn-seal:disabled { opacity: 0.45; cursor: not-allowed; } .btn-primary:disabled { opacity: 0.45; cursor: not-allowed; }
.btn-line { background: var(--card); border: 1px solid var(--border-strong); color: var(--foreground); box-shadow: var(--shadow-sm); } .btn-line { background: var(--card); border: 1px solid var(--border-strong); color: var(--foreground); box-shadow: var(--shadow-sm); }
.btn-line:hover { background: var(--muted); } .btn-line:hover { background: var(--muted); }
.btn-star { background: none; border: 1px solid var(--border-strong); color: var(--muted-foreground); } .btn-star { background: none; border: 1px solid var(--border-strong); color: var(--muted-foreground); }
.btn-star:hover { color: var(--accent-ink); border-color: var(--accent); } .btn-star:hover { color: var(--accent-ink); border-color: var(--accent); }
.btn-star[aria-pressed="true"] { color: var(--accent-ink); border-color: var(--accent); background: var(--accent-soft); font-weight: 600; } .btn-star[aria-pressed="true"] { color: var(--accent-ink); border-color: var(--accent); background: var(--accent-soft); font-weight: 600; }
.btn-seal:active, .btn-line:active, .btn-star:active { scale: 0.98; } .btn-primary:active, .btn-line:active, .btn-star:active { scale: 0.98; }
/* ── 모달: 스플릿. 표지 = 잉크판, 액센트 도장. ── */ /* ── 모달: 스플릿. 정보 패널과 핵심 동작을 분리한다. ── */
.modal { .modal {
margin: auto; margin: auto;
width: min(94vw, 860px); width: min(94vw, 860px);
@ -427,19 +428,16 @@ tbody tr:hover { background: var(--muted); }
display: flex; flex-direction: column; display: flex; flex-direction: column;
overflow: hidden; overflow: hidden;
} }
.mcover-mark { .mcover-label {
position: absolute; right: -18px; bottom: -24px; display: inline-flex;
font-size: 8rem; font-weight: 600; line-height: 1; align-items: center;
color: var(--panel-mark); width: fit-content;
pointer-events: none; user-select: none; color: var(--panel-muted);
} border: 1px solid var(--panel-line);
.mcover-seal { padding: 3px 6px;
width: 44px; height: 44px; font-size: var(--step--2);
display: inline-flex; align-items: center; justify-content: center; font-weight: 600;
font-size: var(--step-2); font-weight: 600; letter-spacing: 0.12em;
color: var(--on-accent);
background: var(--accent);
border-radius: var(--radius);
margin-bottom: var(--space-4); margin-bottom: var(--space-4);
} }
.mcover-name { font-size: var(--step-3); line-height: 1.25; font-weight: 600; letter-spacing: -0.02em; word-break: keep-all; } .mcover-name { font-size: var(--step-3); line-height: 1.25; font-weight: 600; letter-spacing: -0.02em; word-break: keep-all; }
@ -532,11 +530,10 @@ tbody tr:hover { background: var(--muted); }
@media (max-width: 680px) { @media (max-width: 680px) {
.mspread { grid-template-columns: minmax(0, 1fr); } .mspread { grid-template-columns: minmax(0, 1fr); }
.mcover { padding: var(--space-4); flex-direction: row; align-items: center; gap: var(--space-3); } .mcover { padding: var(--space-4); flex-direction: row; align-items: center; gap: var(--space-3); }
.mcover-seal { width: 40px; height: 40px; margin-bottom: 0; font-size: var(--step-0); } .mcover-label { margin-bottom: 0; }
.mcover-name { font-size: var(--step-2); } .mcover-name { font-size: var(--step-2); }
.mcover-meta { margin-top: 2px; } .mcover-meta { margin-top: 2px; }
.mcover-stats, .mcover-foot { display: none; } .mcover-stats, .mcover-foot { display: none; }
.mcover-mark { font-size: 5rem; }
.msheet { padding: var(--space-4); min-height: 0; } .msheet { padding: var(--space-4); min-height: 0; }
.course-row { flex-direction: column; align-items: flex-start; gap: var(--space-2); } .course-row { flex-direction: column; align-items: flex-start; gap: var(--space-2); }
.c-side { text-align: left; display: flex; gap: var(--space-3); align-items: center; } .c-side { text-align: left; display: flex; gap: var(--space-3); align-items: center; }
@ -644,7 +641,7 @@ kbd {
thead th, tbody tr { break-inside: avoid; } thead th, tbody tr { break-inside: avoid; }
tbody tr:hover { background: none; } tbody tr:hover { background: none; }
.tt { min-width: 0; } .tt { min-width: 0; }
.tt-block, .cd-slot, .eval-seg, .c-seal, .c-dept, .no-kind { .tt-block, .cd-slot, .eval-seg, .c-enrolled, .c-dept, .no-kind {
print-color-adjust: exact; -webkit-print-color-adjust: exact; print-color-adjust: exact; -webkit-print-color-adjust: exact;
} }
@ -739,7 +736,7 @@ kbd {
@media (pointer: coarse) { @media (pointer: coarse) {
.chip, .give-btn, .btn-line { min-height: 44px; } .chip, .give-btn, .btn-line { min-height: 44px; }
.icon-btn, .bell, .modal-close { width: 44px; height: 44px; } .icon-btn, .bell, .modal-close { width: 44px; height: 44px; }
.btn-seal, .btn-star { min-height: 44px; } .btn-primary, .btn-star { min-height: 44px; }
#q { min-height: 44px; } #q { min-height: 44px; }
} }

View file

@ -1,8 +1,8 @@
/* ========================================================================== /* ==========================================================================
두레대학 수강신청 — 디자인 토큰 (v3: 프로덕션 콘솔) 두레대학 수강신청 — 디자인 토큰 (v3: 프로덕션 콘솔)
프리셋: production-console — shadcn/ui (new-york) zinc 스키마 + 벽돌 액센트 1. 프리셋: production-console — shadcn/ui (new-york) zinc 스키마 + 벽돌 액센트 1.
컨셉 한 문장: 실제 대학이 쓰는 것 같은 수강신청 — 도장은 액센트 하나로 남고 컨셉 한 문장: 실제 대학이 쓰는 것 같은 수강신청 — 상태 표식은 하나로,
나머지는 완성도로 말한다. 나머지는 정보 밀도와 반응으로 말한다.
레퍼런스: shadcn/ui 디자인 시스템. 종이/배판 컨셉은 철회(사용자 판정). 레퍼런스: shadcn/ui 디자인 시스템. 종이/배판 컨셉은 철회(사용자 판정).
하지 않은 것: 신문 배판 마스트헤드, 세리프 마스트, 전공별 색, 진행바 게이지. 하지 않은 것: 신문 배판 마스트헤드, 세리프 마스트, 전공별 색, 진행바 게이지.
========================================================================== */ ========================================================================== */
@ -36,7 +36,7 @@
--primary: #18181b; --primary: #18181b;
--primary-foreground: #fafafa; --primary-foreground: #fafafa;
--ring: #a1a1aa; --ring: #a1a1aa;
--accent: #c2410c; /* 벽돌 — 申 도장과 신청 액션만 */ --accent: #c2410c; /* 벽돌색 — 신청 상태와 핵심 동작만 */
--accent-soft: #fff7ed; --accent-soft: #fff7ed;
--accent-ink: #9a3412; --accent-ink: #9a3412;
--on-accent: #ffffff; /* 벽돌·남색 배경 위 잉크 */ --on-accent: #ffffff; /* 벽돌·남색 배경 위 잉크 */

View file

@ -50,7 +50,7 @@
<main id="main"> <main id="main">
<!-- <!--
스페시멘 히어로 — 파운드리의 첫 화면은 설명이 아니라 글자다. 스페시멘 히어로 — 파운드리의 첫 화면은 설명이 아니라 글자다.
'한' 한 글자를 페이지에서 가장 크게, 인주각과 함께. '한' 한 글자를 페이지에서 가장 크게, 글자 자체의 비례로 보여준다.
--> -->
<section class="hero" aria-labelledby="hero-h"> <section class="hero" aria-labelledby="hero-h">
<div class="wrap"> <div class="wrap">
@ -107,7 +107,7 @@
<p class="muted">제목·표제용 · 웨이트 600 · 큰 짝에서 벌어짐 0</p> <p class="muted">제목·표제용 · 웨이트 600 · 큰 짝에서 벌어짐 0</p>
</header> </header>
<p class="specimen specimen-display" style="font-family: 'MaruBuri', serif"> <p class="specimen specimen-display" style="font-family: 'MaruBuri', serif">
활자는 붓의 속도를 기억한다 활자는 읽는 리듬을 설계한다
</p> </p>
</article> </article>
</div> </div>
@ -160,7 +160,7 @@
</section> </section>
<!-- <!--
활자 케이스 — 연구소의 물성. 금속 활자와 인주. 활자 케이스 — 연구소의 재료. 금속 활자와 편집 도구.
--> -->
<section id="case" class="section" aria-labelledby="case-h"> <section id="case" class="section" aria-labelledby="case-h">
<div class="wrap case-grid"> <div class="wrap case-grid">
@ -184,13 +184,13 @@
</p> </p>
<figure class="case-photo-b reveal" style="--delay: 80ms"> <figure class="case-photo-b reveal" style="--delay: 80ms">
<img <img
src="./assets/inkstone.webp" src="./assets/metaltype.webp"
alt="연구한 먹이 고인 연적과 옆에 놓인 인주 연판" alt="빛을 받은 금속 활자와 정렬된 활자 케이스"
width="1100" width="1100"
height="825" height="825"
loading="lazy" loading="lazy"
/> />
<figcaption>먹을 갈고 인주를 찍는 자리 — 시험 인쇄 전</figcaption> <figcaption>활자 폭과 여백을 비교하는 편집 자리</figcaption>
</figure> </figure>
</div> </div>
</div> </div>

View file

@ -143,16 +143,6 @@ a {
margin-top: var(--space-4); margin-top: var(--space-4);
position: relative; position: relative;
} }
/* 인주각 — 글리프 옆에 찍히는 도장 */
.hero-glyph::after {
content: "";
position: absolute;
right: 0.12em;
bottom: 0.18em;
width: 0.11em;
height: 0.11em;
background: var(--accent);
}
.hero-side { .hero-side {
margin-top: var(--space-5); margin-top: var(--space-5);
display: grid; display: grid;

View file

@ -6,7 +6,7 @@
스페시멘은 Lorem 이 아니라 실제 문장, 테스터는 즉시 반영. 스페시멘은 Lorem 이 아니라 실제 문장, 테스터는 즉시 반영.
하지 않은 것: Lorem ipsum, 서체 카드 3열, '구독하면 웨이트 전부' 마케팅, 하지 않은 것: Lorem ipsum, 서체 카드 3열, '구독하면 웨이트 전부' 마케팅,
책등 세로 내비. 이유: 활자는 보여주는 것이지 포장하는 게 아니다. 책등 세로 내비. 이유: 활자는 보여주는 것이지 포장하는 게 아니다.
강조 인주빨강 #A63D2F — 인장·CTA에만. 배경 채움은 버튼 하나 뿐. 강조 벽돌빨강 #A63D2F — CTA에만. 배경 채움은 버튼 하나 뿐.
========================================================================== */ ========================================================================== */
:root { :root {
@ -23,15 +23,15 @@
--font-display: "MaruBuri", serif; --font-display: "MaruBuri", serif;
--font-body: "Pretendard Variable", Pretendard, -apple-system, sans-serif; --font-body: "Pretendard Variable", Pretendard, -apple-system, sans-serif;
--surface: #f5f1e8; /* 한지 */ --surface: #f5f1e8; /* 따뜻한 중성 바탕 */
--surface-raised: #ede7d9; --surface-raised: #ede7d9;
--ink: #26221d; --ink: #26221d;
--ink-muted: #6a635a; --ink-muted: #6a635a;
--ink-faint: #948b7c; --ink-faint: #948b7c;
--line: #d9d1bf; --line: #d9d1bf;
--accent: #a63d2f; /* 인주 */ --accent: #a63d2f; /* 강조 */
--accent-ink: #f5f1e8; --accent-ink: #f5f1e8;
--accent-hover: #8a3225; /* 인주 눌린 단계 */ --accent-hover: #8a3225; /* 강조 호버 */
--space-1: 0.25rem; --space-1: 0.25rem;
--space-2: 0.5rem; --space-2: 0.5rem;

View file

@ -527,7 +527,7 @@
<header class="view-head"> <header class="view-head">
<div> <div>
<h2 class="v-h" id="fi-h">자료실</h2> <h2 class="v-h" id="fi-h">자료실</h2>
<p class="view-sub">배부 자료의 원장. 배부를 눌렀는지에 따라 도장이 남습니다.</p> <p class="view-sub">배부 자료의 목록. 배부 여부와 일시를 바로 확인할 수 있습니다.</p>
</div> </div>
</header> </header>
@ -546,7 +546,7 @@
<table> <table>
<caption class="sr-only">배부 자료 목록</caption> <caption class="sr-only">배부 자료 목록</caption>
<thead> <thead>
<tr><th scope="col">자료</th><th scope="col">과목</th><th scope="col">종류</th><th scope="col" class="num">크기</th><th scope="col" class="num">조회</th><th scope="col" class="num">날인일</th><th scope="col">배부</th></tr> <tr><th scope="col">자료</th><th scope="col">과목</th><th scope="col">종류</th><th scope="col" class="num">크기</th><th scope="col" class="num">조회</th><th scope="col" class="num">배부일</th><th scope="col">배부</th></tr>
</thead> </thead>
<tbody id="file-body"><!-- JS --></tbody> <tbody id="file-body"><!-- JS --></tbody>
</table> </table>
@ -666,7 +666,7 @@
<div class="mspread"> <div class="mspread">
<div class="mcover"> <div class="mcover">
<span class="mcover-mark" id="stu-mark" aria-hidden="true">가</span> <span class="mcover-mark" id="stu-mark" aria-hidden="true">가</span>
<span class="mcover-seal" id="stu-seal" aria-hidden="true">가</span> <span class="mcover-label" aria-hidden="true">STUDENT</span>
<h2 class="mcover-name" id="stu-dlg-h">수강생</h2> <h2 class="mcover-name" id="stu-dlg-h">수강생</h2>
<p class="mcover-meta" id="stu-meta">주 과목 · 상태</p> <p class="mcover-meta" id="stu-meta">주 과목 · 상태</p>
<dl class="mcover-stats"> <dl class="mcover-stats">
@ -799,7 +799,7 @@
<script> <script>
/* /*
앱 상태는 전부 이 안에 있다. 서버도 localStorage 도 없다 — 데모 원칙. 앱 상태는 전부 이 안에 있다. 서버도 localStorage 도 없다 — 데모 원칙.
뷰 전환 · 검색/필터 · 출석 순환 · 배부 도장 · 리포트 계산까지 뷰 전환 · 검색/필터 · 출석 순환 · 배부 상태 · 리포트 계산까지
네이티브 기능 위에 얹은 얇은 JS. 네이티브 기능 위에 얹은 얇은 JS.
*/ */
const STUDENTS = [ const STUDENTS = [
@ -1279,7 +1279,6 @@
if (!btn || btn.classList.contains("att-btn")) return; if (!btn || btn.classList.contains("att-btn")) return;
const i = +btn.dataset.stu; const i = +btn.dataset.stu;
const s = STUDENTS[i]; const s = STUDENTS[i];
document.getElementById("stu-seal").textContent = s.name[0];
document.getElementById("stu-mark").textContent = s.name[0]; document.getElementById("stu-mark").textContent = s.name[0];
document.getElementById("stu-dlg-h").innerHTML = `<span class="hl">${s.name}</span>`; document.getElementById("stu-dlg-h").innerHTML = `<span class="hl">${s.name}</span>`;
document.getElementById("stu-meta").textContent = `${s.sub} · ${s.status} · 개강 2026-03-02`; document.getElementById("stu-meta").textContent = `${s.sub} · ${s.status} · 개강 2026-03-02`;

View file

@ -489,13 +489,14 @@ tbody tr:hover { background: var(--muted); }
color: var(--panel-mark); color: var(--panel-mark);
pointer-events: none; user-select: none; pointer-events: none; user-select: none;
} }
.mcover-seal { .mcover-label {
width: 44px; height: 44px; display: inline-flex; align-items: center;
display: inline-flex; align-items: center; justify-content: center; width: fit-content;
font-size: var(--step-2); font-weight: 600; color: var(--panel-muted);
color: var(--on-accent); border: 1px solid var(--panel-line);
background: var(--accent); padding: 3px 6px;
border-radius: var(--radius); font-size: var(--step--2); font-weight: 600;
letter-spacing: 0.12em;
margin-bottom: var(--space-4); margin-bottom: var(--space-4);
} }
.mcover-name { .mcover-name {
@ -626,7 +627,7 @@ tbody tr:hover { background: var(--muted); }
@media (max-width: 680px) { @media (max-width: 680px) {
.mspread { grid-template-columns: minmax(0, 1fr); } .mspread { grid-template-columns: minmax(0, 1fr); }
.mcover { padding: var(--space-4); flex-direction: row; align-items: center; gap: var(--space-3); } .mcover { padding: var(--space-4); flex-direction: row; align-items: center; gap: var(--space-3); }
.mcover-seal { width: 40px; height: 40px; margin-bottom: 0; font-size: var(--step-1); } .mcover-label { margin-bottom: 0; }
.mcover-name { font-size: var(--step-2); } .mcover-name { font-size: var(--step-2); }
.mcover-meta { margin-top: 2px; } .mcover-meta { margin-top: 2px; }
.mcover-stats, .mcover-foot { display: none; } .mcover-stats, .mcover-foot { display: none; }

View file

@ -3,12 +3,12 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>향연 (享煙) · 자연 침향 아카이브</title> <title>향연 · 자연 침향 아카이브</title>
<meta <meta
name="description" name="description"
content="연기는 흩어지고 향기만 머뭅니다. 자연 침향목의 수지 함량과 숙성 연도를 기록으로 남기는 향 아카이브. 서울 삼청동." content="연기는 흩어지고 향기만 머뭅니다. 자연 침향목의 수지 함량과 숙성 연도를 기록으로 남기는 향 아카이브. 서울 삼청동."
/> />
<meta property="og:title" content="향연 (享煙) · 자연 침향 아카이브" /> <meta property="og:title" content="향연 · 자연 침향 아카이브" />
<meta property="og:description" content="수지 함량과 숙성 연도를 기록으로 남기는 침향 아카이브." /> <meta property="og:description" content="수지 함량과 숙성 연도를 기록으로 남기는 침향 아카이브." />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta name="theme-color" content="#F6F2E9" /> <meta name="theme-color" content="#F6F2E9" />
@ -30,20 +30,11 @@
</head> </head>
<body> <body>
<!-- 재질: 한지. 아주 옅은 섬유 그레인 — 사진이 아니라 종이 위에 있다는 감각 -->
<svg class="paper" aria-hidden="true">
<filter id="hanji">
<feTurbulence type="fractalNoise" baseFrequency="0.9 0.7" numOctaves="2" seed="7" stitchTiles="stitch" />
<feColorMatrix type="matrix" values="0 0 0 0 0.55 0 0 0 0 0.52 0 0 0 0 0.46 0 0 0 0.05 0" />
</filter>
<rect width="100%" height="100%" filter="url(#hanji)" />
</svg>
<a class="skip" href="#main">본문으로 건너뛰기</a> <a class="skip" href="#main">본문으로 건너뛰기</a>
<header class="site-head"> <header class="site-head">
<div class="wrap head-inner"> <div class="wrap head-inner">
<a class="wordmark" href="#main">향연 <span class="han">享煙</span></a> <a class="wordmark" href="#main">향연</a>
<nav class="nav" aria-label="섹션"> <nav class="nav" aria-label="섹션">
<a href="#wood">향목</a> <a href="#wood">향목</a>
<a href="#aging">숙성</a> <a href="#aging">숙성</a>
@ -77,13 +68,13 @@
<figure class="hero-photo reveal" style="--delay: 120ms"> <figure class="hero-photo reveal" style="--delay: 120ms">
<img <img
src="./assets/smoke.webp" src="./assets/smoke.webp"
alt="밝은 한지 위 검은 석 받침대에서 한 줄기 침향 연기가 곧게 피어오르는 모습" alt="밝은 배경의 검은 받침대에서 한 줄기 침향 연기가 곧게 피어오르는 모습"
width="900" width="900"
height="1200" height="1200"
loading="eager" loading="eager"
fetchpriority="high" fetchpriority="high"
/> />
<figcaption class="vertical">침향 한 줄기 · 삼청동 아카이브룸</figcaption> <figcaption>침향 한 줄기 · 삼청동 아카이브룸</figcaption>
</figure> </figure>
</div> </div>
</section> </section>
@ -233,7 +224,7 @@
</div> </div>
</dl> </dl>
</div> </div>
<p class="vertical visit-mark">향연 享煙</p> <p class="visit-mark">향연</p>
</div> </div>
</section> </section>
</main> </main>

View file

@ -1,7 +1,7 @@
@import url("./tokens.css"); @import url("./tokens.css");
/* ========================================================================== /* ==========================================================================
향연 — quiet-luxury. 한지 위의 장부. 향연 — quiet-luxury. 밝은 편집면 위의 기록.
========================================================================== */ ========================================================================== */
*, *,
@ -33,20 +33,6 @@ body {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
/* 한지 그레인 — 본문 뒤 전면, 알파 0.05. 이 페이지의 유일한 재질 효과 */
.paper {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
}
body > *:not(.paper) {
position: relative;
z-index: 2;
}
:where(h1, h2, h3) { :where(h1, h2, h3) {
font-family: var(--font-display); font-family: var(--font-display);
font-weight: 500; font-weight: 500;
@ -121,13 +107,6 @@ a {
margin-inline: auto; margin-inline: auto;
} }
/* 세로쓰기 — 이 스튜디오의 물성 어휘. 목요일의 화원과 같은 결, 다른 용도 */
.vertical {
writing-mode: vertical-rl;
text-orientation: upright;
letter-spacing: 0.3em;
}
/* ── 헤더 ─────────────────────────────────────────────────────────────── */ /* ── 헤더 ─────────────────────────────────────────────────────────────── */
.site-head { .site-head {
@ -219,8 +198,8 @@ a {
.hero-photo { .hero-photo {
position: relative; position: relative;
display: grid; display: grid;
grid-template-columns: auto auto; grid-template-columns: minmax(0, 1fr);
gap: var(--space-4); gap: var(--space-2);
justify-content: end; justify-content: end;
align-items: start; align-items: start;
} }
@ -233,7 +212,7 @@ a {
.hero-photo figcaption { .hero-photo figcaption {
font-size: var(--step--1); font-size: var(--step--1);
color: var(--ink-faint); color: var(--ink-faint);
height: 14rem; letter-spacing: 0.06em;
} }
/* ── 섹션 공통 ────────────────────────────────────────────────────────── */ /* ── 섹션 공통 ────────────────────────────────────────────────────────── */
@ -382,7 +361,8 @@ tbody th {
font-family: var(--font-display); font-family: var(--font-display);
color: var(--ink-faint); color: var(--ink-faint);
font-size: var(--step--1); font-size: var(--step--1);
height: 11rem; letter-spacing: 0.08em;
align-self: end;
} }
/* ── 등장 모션 — 다른 페이지보다 느리다 ──────────────────────────────── */ /* ── 등장 모션 — 다른 페이지보다 느리다 ──────────────────────────────── */
@ -416,7 +396,6 @@ tbody th {
} }
.hero-photo { .hero-photo {
max-width: 24rem; max-width: 24rem;
grid-template-columns: 1fr auto;
justify-content: start; justify-content: start;
} }
.visit-grid { .visit-grid {

View file

@ -1,7 +1,7 @@
/* ========================================================================== /* ==========================================================================
향연 (享煙) — 디자인 토큰 향연 — 디자인 토큰
프리셋: quiet-luxury 프리셋: quiet-luxury
컨셉 한 문장: 한지 위의 장부 — 여백이 주인공이고 연기 한 줄기가 흔적이다. 컨셉 한 문장: 밝은 편집면 위의 기록 — 여백이 주인공이고 연기 한 줄기가 흔적이다.
레퍼런스: 방향은 dogfood-incense(단, 그 파일의 6축 수치는 audit-references.md 레퍼런스: 방향은 dogfood-incense(단, 그 파일의 6축 수치는 audit-references.md
에서 날조로 판명 — 토큰 근거로 쓰지 않는다). 사이트 선택(정향·아카이브 에서 날조로 판명 — 토큰 근거로 쓰지 않는다). 사이트 선택(정향·아카이브
계열의 절제)만 방향 앵커로 쓰고, 값은 브리프에서 다시 정했다. 계열의 절제)만 방향 앵커로 쓰고, 값은 브리프에서 다시 정했다.
@ -23,14 +23,14 @@
--font-display: "MaruBuri", serif; --font-display: "MaruBuri", serif;
--font-body: "Pretendard Variable", Pretendard, -apple-system, sans-serif; --font-body: "Pretendard Variable", Pretendard, -apple-system, sans-serif;
/* 색 — 한지 2단계 + 잉크 3단계 + 적갈 1. 정확한 유약색이 아니라 '건조한 종이' */ /* 색 — 따뜻한 중성 바탕 2단계 + 텍스트 3단계 + 적갈 1. */
--surface: #f6f2e9; --surface: #f6f2e9;
--surface-raised: #efe9dc; --surface-raised: #efe9dc;
--ink: #2b2724; --ink: #2b2724;
--ink-muted: #6d655c; --ink-muted: #6d655c;
--ink-faint: #9a9083; --ink-faint: #9a9083;
--line: #ddd4c3; --line: #ddd4c3;
--accent: #7c3b24; /* 적갈 — 잉크가 아니라 인주의 위험한 사촌 */ --accent: #7c3b24; /* 적갈 — 링크와 핵심 동작만 */
--accent-hover: #5f2c1a; --accent-hover: #5f2c1a;
--space-1: 0.25rem; --space-1: 0.25rem;

View file

@ -42,7 +42,7 @@
<header class="site-head"> <header class="site-head">
<div class="wrap head-inner"> <div class="wrap head-inner">
<a class="wordmark" href="./index.html"> <a class="wordmark" href="./index.html">
MATERIA <span class="wordmark-han">空間素材</span> MATERIA
</a> </a>
<nav class="nav" aria-label="메인 메뉴"> <nav class="nav" aria-label="메인 메뉴">
@ -197,7 +197,7 @@
<h3 class="work-card-title">성북동 포레스트 레지던스</h3> <h3 class="work-card-title">성북동 포레스트 레지던스</h3>
<div class="work-card-type">Private Residence · 340㎡ · 자연석 화강석 & 벨기에 리넨</div> <div class="work-card-type">Private Residence · 340㎡ · 자연석 화강석 & 벨기에 리넨</div>
<p class="work-card-desc"> <p class="work-card-desc">
북악산 사계절의 숲 풍경을 차경(借景)으로 끌어들이고, 실내에는 자연석 아일랜드와 수공예 패브릭을 배치한 친환경 럭셔리 주거. 북악산 사계절의 숲 풍경을 차경 기법으로 끌어들이고, 실내에는 자연석 아일랜드와 수공예 패브릭을 배치한 친환경 럭셔리 주거.
</p> </p>
<div class="work-card-actions"> <div class="work-card-actions">
<button <button

View file

@ -353,7 +353,7 @@
<header class="site-head"> <header class="site-head">
<div class="wrap head-inner"> <div class="wrap head-inner">
<a href="../../index.html" class="wordmark" aria-label="MATERIA 홈"> <a href="../../index.html" class="wordmark" aria-label="MATERIA 홈">
MATERIA <span class="wordmark-han">空間素材</span> MATERIA
</a> </a>
<nav class="nav" aria-label="주요 메뉴"> <nav class="nav" aria-label="주요 메뉴">
<a href="./index.html" class="active" style="color: var(--accent); font-weight: 600;">24개 공간 갤러리</a> <a href="./index.html" class="active" style="color: var(--accent); font-weight: 600;">24개 공간 갤러리</a>
@ -577,7 +577,7 @@
<div class="gallery-card-body"> <div class="gallery-card-body">
<span class="gallery-card-zone">Zone B · Guest Suite</span> <span class="gallery-card-zone">Zone B · Guest Suite</span>
<h3 class="gallery-card-title">게스트 침실 스위트</h3> <h3 class="gallery-card-title">게스트 침실 스위트</h3>
<p class="gallery-card-desc">따뜻한 라임워시 도장 벽체와 천연 원목 가구의 안락한 조화.</p> <p class="gallery-card-desc">따뜻한 라임워시 마감 벽체와 천연 원목 가구의 안락한 조화.</p>
</div> </div>
</div> </div>
@ -624,8 +624,8 @@
</div> </div>
<div class="gallery-card-body"> <div class="gallery-card-body">
<span class="gallery-card-zone">Zone C · Tea Alcove</span> <span class="gallery-card-zone">Zone C · Tea Alcove</span>
<h3 class="gallery-card-title">명상 다도실 (Tea Room)</h3> <h3 class="gallery-card-title">조용한 독서실 (Reading Alcove)</h3>
<p class="gallery-card-desc">천연 한지 스크린을 투과하는 부드러운 격자 채광과 원목 다도상.</p> <p class="gallery-card-desc">반투명 패브릭 스크린을 통과하는 부드러운 격자 채광과 낮은 원목 테이블.</p>
</div> </div>
</div> </div>
@ -768,8 +768,8 @@
</div> </div>
<div class="gallery-card-body"> <div class="gallery-card-body">
<span class="gallery-card-zone">Zone F · Detail</span> <span class="gallery-card-zone">Zone F · Detail</span>
<h3 class="gallery-card-title">전통 장부 맞춤 마스터 목공</h3> <h3 class="gallery-card-title">맞춤 정밀 조인트 목공</h3>
<p class="gallery-card-desc">못 없이 체결되는 장인 직영 정밀 결구 구조의 하드웨어.</p> <p class="gallery-card-desc">숨은 체결부로 완성한 정밀 패널 조인트와 하드웨어.</p>
</div> </div>
</div> </div>
</div> </div>
@ -868,11 +868,11 @@
{ img: '../../assets/hannam-05-master-bedroom.webp', zone: 'Zone B · Master Suite', title: '마스터 베드룸 스위트', desc: '천연 리넨 헤드보드 월과 간접 라인 조명으로 완성한 정숙한 침실.' }, { img: '../../assets/hannam-05-master-bedroom.webp', zone: 'Zone B · Master Suite', title: '마스터 베드룸 스위트', desc: '천연 리넨 헤드보드 월과 간접 라인 조명으로 완성한 정숙한 침실.' },
{ img: '../../assets/hannam-06-spa-bathroom.webp', zone: 'Zone B · Spa Bath', title: '화산석 통원석 스파 배스', desc: '자연석을 통째로 깎아낸 원석 욕조와 독립형 파티나 브론즈 수전.' }, { img: '../../assets/hannam-06-spa-bathroom.webp', zone: 'Zone B · Spa Bath', title: '화산석 통원석 스파 배스', desc: '자연석을 통째로 깎아낸 원석 욕조와 독립형 파티나 브론즈 수전.' },
{ img: '../../assets/hannam-13-powder-room.webp', zone: 'Zone B · Powder Room', title: '석재 세면대 파우더룸', desc: '조각적인 통라임스톤 세면대와 수직 슬릿 조명이 빚은 명암.' }, { img: '../../assets/hannam-13-powder-room.webp', zone: 'Zone B · Powder Room', title: '석재 세면대 파우더룸', desc: '조각적인 통라임스톤 세면대와 수직 슬릿 조명이 빚은 명암.' },
{ img: '../../assets/hannam-14-guest-suite.webp', zone: 'Zone B · Guest Suite', title: '게스트 침실 스위트', desc: '따뜻한 라임워시 도장 벽체와 천연 원목 가구의 안락한 조화.' }, { img: '../../assets/hannam-14-guest-suite.webp', zone: 'Zone B · Guest Suite', title: '게스트 침실 스위트', desc: '따뜻한 라임워시 마감 벽체와 천연 원목 가구의 안락한 조화.' },
{ img: '../../assets/hannam-07-library-audio.webp', zone: 'Zone C · Library', title: '서재 & 하이파이 오디오 룸', desc: '음향 반사를 고려한 훈증 오크 책장과 아날로그 턴테이블 석재 콘솔.' }, { img: '../../assets/hannam-07-library-audio.webp', zone: 'Zone C · Library', title: '서재 & 하이파이 오디오 룸', desc: '음향 반사를 고려한 훈증 오크 책장과 아날로그 턴테이블 석재 콘솔.' },
{ img: '../../assets/hannam-11-dressing-room.webp', zone: 'Zone C · Dressing Room', title: '비스포크 워크인 드레스룸', desc: '플루티드 브론즈 유리장과 시계/주얼리 수납 아일랜드 쇼케이스.' }, { img: '../../assets/hannam-11-dressing-room.webp', zone: 'Zone C · Dressing Room', title: '비스포크 워크인 드레스룸', desc: '플루티드 브론즈 유리장과 시계/주얼리 수납 아일랜드 쇼케이스.' },
{ img: '../../assets/hannam-12-wine-cellar.webp', zone: 'Zone C · Wine Cellar', title: '항온항습 글래스 와인 셀러', desc: '백라이트 트래버틴 벽체와 브론즈 프레임 유리 파티션 셀러.' }, { img: '../../assets/hannam-12-wine-cellar.webp', zone: 'Zone C · Wine Cellar', title: '항온항습 글래스 와인 셀러', desc: '백라이트 트래버틴 벽체와 브론즈 프레임 유리 파티션 셀러.' },
{ img: '../../assets/hannam-20-tea-room.webp', zone: 'Zone C · Tea Alcove', title: '명상 다도실 (Tea Room)', desc: '천연 한지 스크린을 투과하는 부드러운 격자 채광과 원목 다도상.' }, { img: '../../assets/hannam-20-tea-room.webp', zone: 'Zone C · Reading Alcove', title: '조용한 독서실 (Reading Alcove)', desc: '반투명 패브릭 스크린을 통과하는 부드러운 격자 채광과 낮은 원목 테이블.' },
{ img: '../../assets/hannam-08-terrace.webp', zone: 'Zone D · Terrace', title: '루프탑 프라이빗 테라스', desc: '대형 트래버틴 페이버와 조형 소나무 분재, 한강 석양 조망.' }, { img: '../../assets/hannam-08-terrace.webp', zone: 'Zone D · Terrace', title: '루프탑 프라이빗 테라스', desc: '대형 트래버틴 페이버와 조형 소나무 분재, 한강 석양 조망.' },
{ img: '../../assets/hannam-09-foyer.webp', zone: 'Zone D · Entrance', title: '미니멀 입구 현관 (Foyer)', desc: '거대한 화강석 벤치 원석과 플러시 히든 피벗 도어 시스템.' }, { img: '../../assets/hannam-09-foyer.webp', zone: 'Zone D · Entrance', title: '미니멀 입구 현관 (Foyer)', desc: '거대한 화강석 벤치 원석과 플러시 히든 피벗 도어 시스템.' },
{ img: '../../assets/hannam-22-lightwell.webp', zone: 'Zone D · Lightwell', title: '실내 석경 중정 (Zen Lightwell)', desc: '천장 보이드에서 쏟아지는 수직 채광과 백자갈 디딤돌 정원.' }, { img: '../../assets/hannam-22-lightwell.webp', zone: 'Zone D · Lightwell', title: '실내 석경 중정 (Zen Lightwell)', desc: '천장 보이드에서 쏟아지는 수직 채광과 백자갈 디딤돌 정원.' },
@ -884,7 +884,7 @@
{ img: '../../assets/hannam-02-blueprint.webp', zone: 'Zone F · Blueprint', title: '86평 복층 건축 평면 설계도', desc: '동선 효율과 채광축을 정밀하게 구획한 마스터 아키텍처 드래프트.' }, { img: '../../assets/hannam-02-blueprint.webp', zone: 'Zone F · Blueprint', title: '86평 복층 건축 평면 설계도', desc: '동선 효율과 채광축을 정밀하게 구획한 마스터 아키텍처 드래프트.' },
{ img: '../../assets/hannam-23-light-simulation.webp', zone: 'Zone F · Analysis', title: '3D 태양광 입사 조도 분석', desc: '하지 및 춘분 정오 55° 고도각에 따른 Lux 분포 및 음영 계산도.' }, { img: '../../assets/hannam-23-light-simulation.webp', zone: 'Zone F · Analysis', title: '3D 태양광 입사 조도 분석', desc: '하지 및 춘분 정오 55° 고도각에 따른 Lux 분포 및 음영 계산도.' },
{ img: '../../assets/hannam-19-joinery.webp', zone: 'Zone F · Detail', title: '5mm 음각 섀도우 갭 조인트', desc: '석재와 원목 패널 사이의 0.5mm 오차 없는 스위스 규격 결합부.' }, { img: '../../assets/hannam-19-joinery.webp', zone: 'Zone F · Detail', title: '5mm 음각 섀도우 갭 조인트', desc: '석재와 원목 패널 사이의 0.5mm 오차 없는 스위스 규격 결합부.' },
{ img: '../../assets/hannam-24-timber-joinery.webp', zone: 'Zone F · Detail', title: '전통 장부 맞춤 마스터 목공', desc: '못 없이 체결되는 장인 직영 정밀 결구 구조의 하드웨어.' } { img: '../../assets/hannam-24-timber-joinery.webp', zone: 'Zone F · Detail', title: '맞춤 정밀 조인트 목공', desc: '숨은 체결부로 완성한 정밀 패널 조인트와 하드웨어.' }
]; ];
// 3. Filter Chips Logic // 3. Filter Chips Logic

View file

@ -3,12 +3,12 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>무월 (霧月) · 싱글오리진 원두 정기구독</title> <title>무월 · 싱글오리진 원두 정기구독</title>
<meta <meta
name="description" name="description"
content="원두마다 산미·단맛·바디를 같은 척도로 측정해 적고, 생두 매입가를 공개합니다. 격주 화요일 로스팅, 서울 서촌." content="원두마다 산미·단맛·바디를 같은 척도로 측정해 적고, 생두 매입가를 공개합니다. 격주 화요일 로스팅, 서울 서촌."
/> />
<meta property="og:title" content="무월 (霧月) · 싱글오리진 원두 정기구독" /> <meta property="og:title" content="무월 · 싱글오리진 원두 정기구독" />
<meta <meta
property="og:description" property="og:description"
content="측정된 좌표로 원두를 고르는 격주 구독. 생두 매입 원장 공개." content="측정된 좌표로 원두를 고르는 격주 구독. 생두 매입 원장 공개."
@ -43,9 +43,7 @@
<header class="site-head"> <header class="site-head">
<div class="wrap head-inner"> <div class="wrap head-inner">
<a class="wordmark" href="#main"> <a class="wordmark" href="#main">무월</a>
무월 <span class="wordmark-han">霧月</span>
</a>
<nav class="nav" aria-label="섹션"> <nav class="nav" aria-label="섹션">
<a href="#scale">척도</a> <a href="#scale">척도</a>
<a href="#ledger">원장</a> <a href="#ledger">원장</a>

View file

@ -1,5 +1,5 @@
/* ========================================================================== /* ==========================================================================
무월 (霧月 · MUWOL) — 디자인 토큰 무월 (MUWOL) — 디자인 토큰
프리셋: quiet-commerce 프리셋: quiet-commerce
컨셉 한 문장: 종이 원장처럼 정직한 구독 — 화면에는 사양과 실측만. 컨셉 한 문장: 종이 원장처럼 정직한 구독 — 화면에는 사양과 실측만.
레퍼런스: R1 coffeecollective.dk(구조·가격 노출·거래 투명성) 레퍼런스: R1 coffeecollective.dk(구조·가격 노출·거래 투명성)

View file

@ -84,7 +84,7 @@
</li> </li>
<li class="work work-b reveal" style="--delay: 60ms"> <li class="work work-b reveal" style="--delay: 60ms">
<p class="work-year mono">2025</p> <p class="work-year mono">2025</p>
<h3>무(無)의 스펙트럼</h3> <h3>무의 스펙트럼</h3>
<p> <p>
아무 소리도 없는 방의 바닥 소음(−62dB)을 증폭해 들려주는 아무 소리도 없는 방의 바닥 소음(−62dB)을 증폭해 들려주는
퍼포먼스. 침묵에는 평균 진폭이 있다. 퍼포먼스. 침묵에는 평균 진폭이 있다.

View file

@ -3,12 +3,12 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>새벽청과 (曉靑果) — 오늘 새벽 두 시의 값</title> <title>새벽청과 — 오늘 새벽 두 시의 값</title>
<meta <meta
name="description" name="description"
content="새벽 경매에서 낙찰받아 그날 아침에 보냅니다. 품목마다 산지·등급·당도·규격과 오늘 낙찰가를 그대로 적는 청과 직거래." content="새벽 경매에서 낙찰받아 그날 아침에 보냅니다. 품목마다 산지·등급·당도·규격과 오늘 낙찰가를 그대로 적는 청과 직거래."
/> />
<meta property="og:title" content="새벽청과 (曉靑果)" /> <meta property="og:title" content="새벽청과" />
<meta property="og:description" content="값이 매일 바뀐다는 사실을 첫 화면에 적습니다." /> <meta property="og:description" content="값이 매일 바뀐다는 사실을 첫 화면에 적습니다." />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta name="theme-color" content="#16181a" /> <meta name="theme-color" content="#16181a" />
@ -38,7 +38,7 @@
<header class="site-head"> <header class="site-head">
<div class="wrap head-inner"> <div class="wrap head-inner">
<a class="wordmark" href="#top"> <a class="wordmark" href="#top">
새벽청과 <span class="wordmark-han" aria-hidden="true">曉靑果</span> 새벽청과
</a> </a>
<nav class="nav" aria-label="주요 섹션"> <nav class="nav" aria-label="주요 섹션">
<a href="#route">오는 길</a> <a href="#route">오는 길</a>
@ -65,10 +65,10 @@
aria-hidden="true" aria-hidden="true"
/> />
<div class="wrap hero-inner"> <div class="wrap hero-inner">
<p class="hero-stamp mono"> <p class="hero-meta mono">
<span>오늘 경매</span> <span>오늘 경매</span>
<time datetime="2026-08-24T02:00+09:00">08월 24일 02:00</time> <time datetime="2026-08-24T02:00+09:00">08월 24일 02:00</time>
<span class="stamp-sep" aria-hidden="true">·</span> <span class="meta-sep" aria-hidden="true">·</span>
<span>하절기 기준</span> <span>하절기 기준</span>
</p> </p>
@ -362,7 +362,7 @@
</dd> </dd>
</div> </div>
<div> <div>
<dt>선과 <span class="term-en mono">選果</span></dt> <dt>선과</dt>
<dd> <dd>
기계로 골라내는 일입니다. 드럼식은 지름으로, 중량식은 무게로 나눕니다. 기계로 골라내는 일입니다. 드럼식은 지름으로, 중량식은 무게로 나눕니다.
같은 상자 안의 알이 고른 이유가 여기 있습니다. 같은 상자 안의 알이 고른 이유가 여기 있습니다.
@ -714,7 +714,7 @@
<footer class="band band-dark site-foot"> <footer class="band band-dark site-foot">
<div class="wrap foot-inner"> <div class="wrap foot-inner">
<p class="foot-name">새벽청과 <span aria-hidden="true">曉靑果</span></p> <p class="foot-name">새벽청과</p>
<p class="foot-line">새벽 경매에서 받아 그날 아침에 보냅니다</p> <p class="foot-line">새벽 경매에서 받아 그날 아침에 보냅니다</p>
<p class="foot-fine"> <p class="foot-fine">
이 페이지는 designpaca 쇼케이스입니다. 실제 영업하는 상호가 아니며, 이 페이지는 designpaca 쇼케이스입니다. 실제 영업하는 상호가 아니며,
@ -739,10 +739,10 @@
.map((part) => [part.type, part.value]), .map((part) => [part.type, part.value]),
); );
const currentMonth = Number(dateParts.month); const currentMonth = Number(dateParts.month);
const stamp = document.querySelector(".hero-stamp time"); const heroMetaTime = document.querySelector(".hero-meta time");
if (stamp) { if (heroMetaTime) {
stamp.dateTime = `${dateParts.year}-${dateParts.month}-${dateParts.day}T02:00+09:00`; heroMetaTime.dateTime = `${dateParts.year}-${dateParts.month}-${dateParts.day}T02:00+09:00`;
stamp.textContent = `${dateParts.month}월 ${dateParts.day}일 02:00`; heroMetaTime.textContent = `${dateParts.month}월 ${dateParts.day}일 02:00`;
} }
/* 판의 한 행을 고르면 같은 값으로 로트 판독기가 바뀐다. */ /* 판의 한 행을 고르면 같은 값으로 로트 판독기가 바뀐다. */

Binary file not shown.

Before

Width:  |  Height:  |  Size: 521 KiB

After

Width:  |  Height:  |  Size: 455 KiB

Before After
Before After

View file

@ -239,7 +239,7 @@ h3 {
z-index: 1; z-index: 1;
} }
.hero-stamp { .hero-meta {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: baseline; align-items: baseline;
@ -250,11 +250,11 @@ h3 {
border-bottom: 1px solid var(--band-line-strong); border-bottom: 1px solid var(--band-line-strong);
} }
.hero-stamp time { .hero-meta time {
color: var(--band-accent); color: var(--band-accent);
} }
.stamp-sep { .meta-sep {
color: var(--band-ink-faint); color: var(--band-ink-faint);
} }

View file

@ -1,5 +1,5 @@
/* ========================================================================== /* ==========================================================================
새벽청과 (曉靑果) — 청과 도매 직거래 · 디자인 토큰 SSOT 새벽청과 — 청과 도매 직거래 · 디자인 토큰 SSOT
컨셉 : 오늘 새벽 두 시의 값 — 값이 매일 바뀐다는 사실을 숨기지 않고 컨셉 : 오늘 새벽 두 시의 값 — 값이 매일 바뀐다는 사실을 숨기지 않고
첫 화면의 주인공으로 삼는다 첫 화면의 주인공으로 삼는다

View file

@ -55,7 +55,7 @@
<section class="hero" aria-labelledby="hero-h"> <section class="hero" aria-labelledby="hero-h">
<div class="hero-inner wrap"> <div class="hero-inner wrap">
<div class="hero-text"> <div class="hero-text">
<p class="vertical hero-role reveal">플로럴 스튜디오 / 서울 서촌 / 2019</p> <p class="hero-role reveal">플로럴 스튜디오 / 서울 서촌 / 2019</p>
<div class="hero-body"> <div class="hero-body">
<h1 id="hero-h" class="hero-h reveal"> <h1 id="hero-h" class="hero-h reveal">
@ -102,7 +102,7 @@
loading="lazy" loading="lazy"
/> />
</figure> </figure>
<p class="vertical hero-tags reveal" style="--delay: 420ms"> <p class="hero-tags reveal" style="--delay: 420ms">
주문 제작 / 정기 구독 / 공간 장식 주문 제작 / 정기 구독 / 공간 장식
</p> </p>
</div> </div>
@ -274,7 +274,7 @@
<p class="visit-note muted"> <p class="visit-note muted">
목요일 외에는 상주하지 않습니다. 다른 요일 방문은 미리 연락 주세요. 목요일 외에는 상주하지 않습니다. 다른 요일 방문은 미리 연락 주세요.
</p> </p>
<p class="vertical visit-mark">목요일의 화원</p> <p class="visit-mark">목요일의 화원</p>
</div> </div>
</section> </section>
</main> </main>

View file

@ -142,16 +142,13 @@ h3 + * {
padding-block: var(--space-8); padding-block: var(--space-8);
} }
/* 세로로 세운 글자. R1 이 여백을 이렇게 쓴다 — /* 가로 정보 라벨. 여백은 글자를 세우지 않고도 충분히 구분된다. */
가로 공간을 먹지 않으면서 빈 곳에 정보를 흘려 넣는다. */ .hero-role,
.vertical { .hero-tags,
writing-mode: vertical-rl; .visit-mark {
font-size: var(--step-label); font-size: var(--step-label);
font-weight: var(--weight-bold); font-weight: var(--weight-bold);
/* 가로쓰기 라벨의 자간(0.14em)을 그대로 쓰면 안 된다. letter-spacing: 0.08em;
세로쓰기에서 자간은 **글자 사이 세로 간격**이 되어 낱글자가 흩어진다.
한글은 자소가 모여 한 글자라 특히 심하다. */
letter-spacing: 0.02em;
color: var(--ink-faint); color: var(--ink-faint);
white-space: nowrap; white-space: nowrap;
} }
@ -287,7 +284,7 @@ h3 + * {
.hero-inner { .hero-inner {
display: grid; display: grid;
/* 텍스트 칼럼이 좁으면 h1 이 3줄로 접힌다(카운트 규칙: 헤드라인 2줄 이하). /* 텍스트 칼럼이 좁으면 h1 이 3줄로 접힌다(카운트 규칙: 헤드라인 2줄 이하).
세로 라벨이 왼쪽에서 자리를 먹으므로 그만큼 더 준다. */ 정보 라벨이 왼쪽에서 자리를 먹으므로 그만큼 더 준다. */
grid-template-columns: minmax(0, 1.06fr) minmax(0, 1fr); grid-template-columns: minmax(0, 1.06fr) minmax(0, 1fr);
gap: var(--space-5); gap: var(--space-5);
align-items: center; align-items: center;
@ -515,14 +512,14 @@ h3 + * {
gap: var(--space-6); gap: var(--space-6);
} }
/* 여기가 실제 사고 지점이었다. .hero-text 가 `auto 1fr` 2칸인데 /* 여기가 실제 사고 지점이었다. .hero-text 가 `auto 1fr` 2칸인데
세로 라벨을 가로로 눕히자 그 긴 문장이 auto 칸을 통째로 먹어 정보 라벨이 긴 문장을 차지하자 auto 칸을 통째로 먹어
h1 칸이 짜부라졌다 — 390px 에서 헤드라인이 6줄, 320px 에서 11줄이 됐다. */ h1 칸이 짜부라졌다 — 390px 에서 헤드라인이 6줄, 320px 에서 11줄이 됐다. */
.hero-text { .hero-text {
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: var(--space-4); gap: var(--space-4);
} }
.hero-role { .hero-role {
writing-mode: horizontal-tb; max-width: 100%;
} }
/* 히어로의 텍스트 요소는 넷을 넘지 않는다(카운트 규칙) */ /* 히어로의 텍스트 요소는 넷을 넘지 않는다(카운트 규칙) */
.hero-tags { .hero-tags {
@ -532,7 +529,7 @@ h3 + * {
aspect-ratio: 1 / 0.9; aspect-ratio: 1 / 0.9;
} }
.visit-mark { .visit-mark {
writing-mode: horizontal-tb; max-width: 100%;
} }
.two, .two,

View file

@ -30,7 +30,7 @@
--font-body: "Pretendard", -apple-system, "Segoe UI", sans-serif; --font-body: "Pretendard", -apple-system, "Segoe UI", sans-serif;
/* 가장 긴 줄이 "그 주에 좋은 꽃만"(한글 7 + 공백 2 ≒ 7.7em)이다. /* 가장 긴 줄이 "그 주에 좋은 꽃만"(한글 7 + 공백 2 ≒ 7.7em)이다.
세로 라벨이 왼쪽에서 60px 을 먹으므로 텍스트 칼럼은 화면 폭의 절반보다 좁다. 정보 라벨이 왼쪽에서 60px 을 먹으므로 텍스트 칼럼은 화면 폭의 절반보다 좁다.
1024px 에서 55.7px 로 잡히자 딱 넘쳐 3줄이 됐다(카운트 규칙 위반). 1024px 에서 55.7px 로 잡히자 딱 넘쳐 3줄이 됐다(카운트 규칙 위반).
기울기를 낮춰 전 구간에서 2줄을 유지한다. */ 기울기를 낮춰 전 구간에서 2줄을 유지한다. */
--step-display: clamp(2.2rem, 1.3rem + 2.9vw, 4.2rem); --step-display: clamp(2.2rem, 1.3rem + 2.9vw, 4.2rem);

View file

@ -12,7 +12,7 @@
- 적용: 두 앱 모두 화면이 6~9개 → **바텀 탭 5 + ‘더보기’ 시트**(나머지 화면). 더보기 시트는 모바일 웹의 정립된 패턴(UIKit의 전통적 More와 같은 결). - 적용: 두 앱 모두 화면이 6~9개 → **바텀 탭 5 + ‘더보기’ 시트**(나머지 화면). 더보기 시트는 모바일 웹의 정립된 패턴(UIKit의 전통적 More와 같은 결).
### 1.2 시트(모달) ### 1.2 시트(모달)
- **Apple HIG — Sheets**: 집중된 자기완结 작업용, 부모 화면 위에 떠오르고 완료 후 복귀. https://developer.apple.com/design/human-interface-guidelines/sheets - **Apple HIG — Sheets**: 집중된 자기 완결 작업용, 부모 화면 위에 떠오르고 완료 후 복귀. https://developer.apple.com/design/human-interface-guidelines/sheets
- **NN/g — Bottom Sheets**: 정의·UX 지침. 썸 존(화면 하단)에 있어 한 손 조작에 최적. https://www.nngroup.com/articles/bottom-sheet/ - **NN/g — Bottom Sheets**: 정의·UX 지침. 썸 존(화면 하단)에 있어 한 손 조작에 최적. https://www.nngroup.com/articles/bottom-sheet/
- **Material — Sheets: bottom**: 모바일에서 대화상자 대안, 드래그 핸들(그래버)은 선택 사항. https://m2.material.io/components/sheets-bottom , https://mobbin.com/glossary/bottom-sheet - **Material — Sheets: bottom**: 모바일에서 대화상자 대안, 드래그 핸들(그래버)은 선택 사항. https://m2.material.io/components/sheets-bottom , https://mobbin.com/glossary/bottom-sheet
- 적용: ≤680px에서 기존 중앙 모달을 **바텀 시트**로(그래버 표시, 상단 라운드, 내부 스크롤, 최대 90dvh, safe-area 준수). - 적용: ≤680px에서 기존 중앙 모달을 **바텀 시트**로(그래버 표시, 상단 라운드, 내부 스크롤, 최대 90dvh, safe-area 준수).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 819 KiB

After

Width:  |  Height:  |  Size: 818 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 820 KiB

After

Width:  |  Height:  |  Size: 842 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 KiB

After

Width:  |  Height:  |  Size: 375 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 KiB

After

Width:  |  Height:  |  Size: 365 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 521 KiB

After

Width:  |  Height:  |  Size: 455 KiB

Before After
Before After

View file

@ -117,10 +117,10 @@ export const WORK: WorkItem[] = [
/* ── 3행: 3열 모자이크 (2 + 2 + 2) ── */ /* ── 3행: 3열 모자이크 (2 + 2 + 2) ── */
{ {
slug: "muwol-coffee", slug: "muwol-coffee",
name: "무월 (霧月)", name: "무월",
kind: "싱글오리진 원두 정기구독", kind: "싱글오리진 원두 정기구독",
concept: "종이 원장처럼 정직한 구독 — 가격은 첫 화면에, 거래 내역은 원장으로 공개", concept: "종이 원장처럼 정직한 구독 — 가격은 첫 화면에, 거래 내역은 원장으로 공개",
risk: "세로 GNB와 카드 대신 원장의 행(行) 문법으로 원두를 나열하고, 구독 CTA는 회색조에서 가장 어두운 반전 블록으로", risk: "세로 GNB와 카드 대신 원장의 행 문법으로 원두를 나열하고, 구독 CTA는 회색조에서 가장 어두운 반전 블록으로",
preset: "quiet-commerce", preset: "quiet-commerce",
date: "2026-08", date: "2026-08",
metrics: ["가격 첫 화면 노출", "앰버 강조 1색", "산미-바디 좌표 플롯", "하드 게이트 12/12"], metrics: ["가격 첫 화면 노출", "앰버 강조 1색", "산미-바디 좌표 플롯", "하드 게이트 12/12"],
@ -147,17 +147,17 @@ export const WORK: WorkItem[] = [
}, },
{ {
slug: "hyang-incense", slug: "hyang-incense",
name: "향연 (享煙)", name: "향연",
kind: "자연 침향 아카이브", kind: "자연 침향 아카이브",
concept: "한지 위의 장부 — 여백이 주인공이고 연기 한 줄기가 흔적이다", concept: "밝은 편집면 위의 기록 — 여백이 주인공이고 연기 한 줄기가 흔적이다",
risk: "상품 사진 대신 사진 없는 목록(텍스트가 상품)과 한지 그레인, 세로쓰기 캡션으로 장부의 물성을 만듭니다", risk: "상품 사진 대신 사진 없는 목록과 가로 정보 라벨로 기록의 밀도를 만듭니다",
preset: "quiet-luxury", preset: "quiet-luxury",
date: "2026-08", date: "2026-08",
metrics: ["본문 17px·1.9행간", "한지 SVG 그레인", "수지 장부 공개", "하드 게이트 12/12"], metrics: ["본문 17px·1.9행간", "중성 배경", "수지 기록 공개", "하드 게이트 12/12"],
href: "/work/hyang-incense/", href: "/work/hyang-incense/",
shot: hyangIncense, shot: hyangIncense,
shotAlt: hyangIncense2, shotAlt: hyangIncense2,
alt: "향연 — 넓은 한지 여백 위에 세로로 피어오르는 연기 사진과 조용한 숙성 장부", alt: "향연 — 넓은 밝은 여백 위에 피어오르는 연기 사진과 조용한 숙성 기록",
size: "sm", size: "sm",
}, },
@ -196,7 +196,7 @@ export const WORK: WorkItem[] = [
/* ── 5행: 3열 모자이크 (2 + 2 + 2) ── */ /* ── 5행: 3열 모자이크 (2 + 2 + 2) ── */
{ {
slug: "baekje-celadon", slug: "baekje-celadon",
name: "비색의 침묵 (翡色之靜)", name: "비색의 침묵",
kind: "고려청자 특별전 큐레이션", kind: "고려청자 특별전 큐레이션",
concept: "암실의 세 개의 방 — 사진이 어두울수록 유약이 가까워진다", concept: "암실의 세 개의 방 — 사진이 어두울수록 유약이 가까워진다",
risk: "전시 포스터 형식을 버리고 빙렬 매크로를 풀블리드 판으로, 상감 접사와 '제1실·제2실' 방 번호 문법으로 구성합니다", risk: "전시 포스터 형식을 버리고 빙렬 매크로를 풀블리드 판으로, 상감 접사와 '제1실·제2실' 방 번호 문법으로 구성합니다",
@ -228,15 +228,15 @@ export const WORK: WorkItem[] = [
slug: "font-foundry", slug: "font-foundry",
name: "한성활자연구소", name: "한성활자연구소",
kind: "본문용 한글 활자 파운드리", kind: "본문용 한글 활자 파운드리",
concept: "글자가 상품이다 — 첫 화면의 거대 글리프와 인주각, 나머지는 물성과 눈금", concept: "글자가 상품이다 — 첫 화면의 거대 글리프, 나머지는 재료와 눈금",
risk: "Lorem ipsum 없는 스페시멘과 자간·행간·크기가 실시간 반영되는 조판 시험대를 전면에 배치합니다", risk: "Lorem ipsum 없는 스페시멘과 자간·행간·크기가 실시간 반영되는 조판 시험대를 전면에 배치합니다",
preset: "editorial-archival", preset: "editorial-archival",
date: "2026-08", date: "2026-08",
metrics: ["시험대 슬라이더 3종", "Lorem 0회", "인주 액센트 1색", "하드 게이트 12/12"], metrics: ["시험대 슬라이더 3종", "Lorem 0회", "벽돌 강조 1색", "하드 게이트 12/12"],
href: "/work/font-foundry/", href: "/work/font-foundry/",
shot: fontFoundry, shot: fontFoundry,
shotAlt: fontFoundry2, shotAlt: fontFoundry2,
alt: "한성활자연구소 — 한지 톤 위 거대한 한글 글리프와 슬라이더 3개의 조판 시험대", alt: "한성활자연구소 — 따뜻한 중성 바탕 위 거대한 한글 글리프와 슬라이더 3개의 조판 시험대",
size: "sm", size: "sm",
}, },
@ -245,8 +245,8 @@ export const WORK: WorkItem[] = [
slug: "gaon-lms", slug: "gaon-lms",
name: "가온 학적부", name: "가온 학적부",
kind: "학습관리 콘솔 · 8화면", kind: "학습관리 콘솔 · 8화면",
concept: "종이 학적부를 화면으로 옮긴 관리 콘솔 — 형광펜 밑줄과 날인이 상태를 말한다", concept: "기록 중심의 관리 콘솔 — 형광펜 밑줄과 상태 라벨이 정보를 구분한다",
risk: "관리 콘솔의 기본값인 파란 사이드바와 카드 그리드를 버리고, 학적부의 행(行) 문법으로 화면 여덟 개를 묶어 짓습니다", risk: "관리 콘솔의 기본값인 파란 사이드바와 카드 그리드를 버리고, 학적부의 행 문법으로 화면 여덟 개를 묶어 짓습니다",
preset: "ledger-console", preset: "ledger-console",
date: "2026-08", date: "2026-08",
metrics: ["뷰 9종 전환", "모바일 바텀내비 + 더보기 시트", "안드로이드 백 키 = 앱 내 뒤로", "safe area · 44px 터치타깃", "홈 설치(PWA) · 인쇄 2종", "채점 범위 검증", "키보드 단축키 1–9 · /"], metrics: ["뷰 9종 전환", "모바일 바텀내비 + 더보기 시트", "안드로이드 백 키 = 앱 내 뒤로", "safe area · 44px 터치타깃", "홈 설치(PWA) · 인쇄 2종", "채점 범위 검증", "키보드 단축키 1–9 · /"],
@ -260,28 +260,28 @@ export const WORK: WorkItem[] = [
slug: "dure-enrollment", slug: "dure-enrollment",
name: "두레대학 수강신청", name: "두레대학 수강신청",
kind: "수강신청 · 6화면", kind: "수강신청 · 6화면",
concept: "공문처럼 읽히는 수강신청 — 명조 제목과 IBM Plex 본문, 벽돌 申 도장이 상태를 누른다", concept: "정돈된 수강신청 — 명료한 제목과 IBM Plex 본문, 벽돌색 상태 라벨이 정보를 구분한다",
risk: "여섯 화면이 하나의 상태(신청·관심)를 공유합니다. 시간 겹침·학점 초과·정원 마감은 도장을 누르기 전에 막습니다", risk: "여섯 화면이 하나의 상태(신청·관심)를 공유합니다. 시간 겹침·학점 초과·정원 마감은 신청 전에 막습니다",
preset: "timetable-ink", preset: "timetable-ink",
date: "2026-08", date: "2026-08",
metrics: ["시간 겹침 즉시 차단", "취소 실행취소", "색인탭 6종 · 모바일 바텀내비", "안드로이드 백 키 = 앱 내 뒤로", "safe area · 44px 터치타깃", "홈 설치(PWA) · 인쇄 2종", "이수 학점 실시간 집계"], metrics: ["시간 겹침 즉시 차단", "취소 실행취소", "색인탭 6종 · 모바일 바텀내비", "안드로이드 백 키 = 앱 내 뒤로", "safe area · 44px 터치타깃", "홈 설치(PWA) · 인쇄 2종", "이수 학점 실시간 집계"],
href: "/work/dure-enrollment/", href: "/work/dure-enrollment/",
shot: dureEnrollment, shot: dureEnrollment,
shotAlt: dureEnrollment2, shotAlt: dureEnrollment2,
alt: "두레대학 수강신청 — 과목 원장 목록과 주홍 도장 블록이 놓이는 주간 시간표 격자", alt: "두레대학 수강신청 — 과목 목록과 벽돌색 상태 라벨이 놓이는 주간 시간표 격자",
size: "md", size: "md",
}, },
/* ── 7행: 신규 쇼케이스 3종 (2 + 2 + 2) ── */ /* ── 7행: 신규 쇼케이스 3종 (2 + 2 + 2) ── */
{ {
slug: "cheongon-care", slug: "cheongon-care",
name: "청온 케어 (淸溫)", name: "청온 케어",
kind: "위생 케어 & 관제 콘솔 · 2모드", kind: "위생 케어 & 관제 콘솔 · 2모드",
concept: "임상 수준의 6단계 위생 프로토콜 — 정찰제 견적과 실시간 배차 관제 콘솔", concept: "임상 수준의 6단계 위생 프로토콜 — 정찰제 견적과 실시간 배차 관제 콘솔",
risk: "고객용 견적 신청 폼과 관리자용 관제 대시보드가 단일 웹앱에서 실시간 상태를 공유하는 풀-루프 인터랙션", risk: "고객용 견적 신청 폼과 관리자용 관제 대시보드가 단일 웹앱에서 실시간 상태를 공유하는 풀-루프 인터랙션",
preset: "ledger-console", preset: "ledger-console",
date: "2026-08", date: "2026-08",
metrics: ["고객·관리자 2모드", "실시간 정찰제 견적기", "32개 QC 체크리스트", "에메랄드 1색", "하드 게이트 14/14"], metrics: ["고객·관리자 2모드", "실시간 정찰제 견적기", "32개 QC 체크리스트", "에메랄드 1색", "하드 게이트 15/15"],
href: "/work/cheongon-care/", href: "/work/cheongon-care/",
subLinks: [ subLinks: [
{ label: "임상 6단계 위생 프로토콜 & ATP 리포트", href: "/work/cheongon-care/protocol/" } { label: "임상 6단계 위생 프로토콜 & ATP 리포트", href: "/work/cheongon-care/protocol/" }
@ -293,13 +293,13 @@ export const WORK: WorkItem[] = [
}, },
{ {
slug: "cask-archive", slug: "cask-archive",
name: "캐스크 아카이브 (樽室)", name: "캐스크 아카이브",
kind: "독립병입자 위스키바 & 도슨트", kind: "독립병입자 위스키바 & 도슨트",
concept: "시간과 배럴이 빚어낸 단 하나의 원액 — 독립병입자 공식 인증 원장과 1:1 도슨트", concept: "시간과 배럴이 빚어낸 단 하나의 원액 — 독립병입자 공식 인증 원장과 1:1 도슨트",
risk: "일반 메뉴판 대신 단일 캐스크 넘버와 통관 필증이 기록된 캐스크 원장 및 향미 필터를 첫 화면에 배치", risk: "일반 메뉴판 대신 단일 캐스크 넘버와 통관 필증이 기록된 캐스크 원장 및 향미 필터를 첫 화면에 배치",
preset: "editorial-archival", preset: "editorial-archival",
date: "2026-08", date: "2026-08",
metrics: ["독립병입자 4종 인증", "실시간 캐스크 원장 검색", "1:1 도슨트 예약", "앰버 골드 1색", "하드 게이트 14/14"], metrics: ["독립병입자 4종 인증", "실시간 캐스크 원장 검색", "1:1 도슨트 예약", "앰버 골드 1색", "하드 게이트 15/15"],
href: "/work/cask-archive/", href: "/work/cask-archive/",
subLinks: [ subLinks: [
{ label: "1:1 위스키 도슨트 테이스팅 마스터클래스", href: "/work/cask-archive/docent/" } { label: "1:1 위스키 도슨트 테이스팅 마스터클래스", href: "/work/cask-archive/docent/" }
@ -311,13 +311,13 @@ export const WORK: WorkItem[] = [
}, },
{ {
slug: "materia-interior", slug: "materia-interior",
name: "마테리아 스튜디오 (空間素材)", name: "마테리아 스튜디오",
kind: "하이엔드 공간 인테리어 건축", kind: "하이엔드 공간 인테리어 건축",
concept: "빛과 트래버틴이 빚어내는 침묵 — 물성과 자연광의 궤적을 설계하는 아틀리에", concept: "빛과 트래버틴이 빚어내는 침묵 — 물성과 자연광의 궤적을 설계하는 아틀리에",
risk: "썸네일 그리드를 버리고 트래버틴·오크·리넨의 실측 물성 스펙과 자연광/야간 조도 앰비언스 실시간 전환을 제공", risk: "썸네일 그리드를 버리고 트래버틴·오크·리넨의 실측 물성 스펙과 자연광/야간 조도 앰비언스 실시간 전환을 제공",
preset: "editorial", preset: "editorial",
date: "2026-08", date: "2026-08",
metrics: ["자연광·야간 조도 전환", "트래버틴 물성 스펙", "더블 베젤 갤러리", "브론즈 1색", "하드 게이트 14/14"], metrics: ["자연광·야간 조도 전환", "트래버틴 물성 스펙", "더블 베젤 갤러리", "브론즈 1색", "하드 게이트 15/15"],
href: "/work/materia-interior/", href: "/work/materia-interior/",
subLinks: [ subLinks: [
{ label: "24개 공간 심층 아카이브 갤러리 (한남 듀플렉스)", href: "/work/materia-interior/projects/hannam-duplex/" } { label: "24개 공간 심층 아카이브 갤러리 (한남 듀플렉스)", href: "/work/materia-interior/projects/hannam-duplex/" }
@ -342,7 +342,7 @@ export const WORK: WorkItem[] = [
"라이트/다크 반전을 토큰 규칙으로", "라이트/다크 반전을 토큰 규칙으로",
"수치는 전부 개발 저장소 출처", "수치는 전부 개발 저장소 출처",
"정확도 지표는 싣지 않음", "정확도 지표는 싣지 않음",
"하드 게이트 14/14", "하드 게이트 15/15",
], ],
href: "/work/rfs-cfd/", href: "/work/rfs-cfd/",
shot: rfsCfd, shot: rfsCfd,
@ -363,7 +363,7 @@ export const WORK: WorkItem[] = [
"한글 폰트 미지정 → Pretendard 지정", "한글 폰트 미지정 → Pretendard 지정",
"현장 전환 JS 0바이트 (라디오 그룹)", "현장 전환 JS 0바이트 (라디오 그룹)",
"신호 적색 1색", "신호 적색 1색",
"하드 게이트 14/14", "하드 게이트 15/15",
], ],
href: "/work/twentyoz-corp/", href: "/work/twentyoz-corp/",
shot: twentyozCorp, shot: twentyozCorp,
@ -373,7 +373,7 @@ export const WORK: WorkItem[] = [
}, },
{ {
slug: "saebyeok-cheonggwa", slug: "saebyeok-cheonggwa",
name: "새벽청과 (曉靑果)", name: "새벽청과",
kind: "청과 도매 직거래", kind: "청과 도매 직거래",
concept: "오늘 새벽 두 시의 값 — 값이 매일 바뀐다는 사실을 첫 화면의 주인공으로 삼는다", concept: "오늘 새벽 두 시의 값 — 값이 매일 바뀐다는 사실을 첫 화면의 주인공으로 삼는다",
risk: "상품 사진 대신 오늘의 시세판이 히어로입니다. 낙찰가와 판매가를 같은 것처럼 적지 않고, 당도를 표시할 수 없는 품목은 '—' 로 둡니다", risk: "상품 사진 대신 오늘의 시세판이 히어로입니다. 낙찰가와 판매가를 같은 것처럼 적지 않고, 당도를 표시할 수 없는 품목은 '—' 로 둡니다",
@ -384,7 +384,7 @@ export const WORK: WorkItem[] = [
"등급·당도·규격 실제 표준규격 근거", "등급·당도·규격 실제 표준규격 근거",
"제철 12개월 띠 · 이번 달 자동 표시", "제철 12개월 띠 · 이번 달 자동 표시",
"새벽 남색 1색 · 사진만 유채색", "새벽 남색 1색 · 사진만 유채색",
"하드 게이트 14/14", "하드 게이트 15/15",
], ],
href: "/work/saebyeok-cheonggwa/", href: "/work/saebyeok-cheonggwa/",
shot: saebyeokCheonggwa, shot: saebyeokCheonggwa,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 149 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 121 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Before After
Before After

View file

@ -13,6 +13,24 @@ const root = path.resolve(here, "..");
const shots = path.join(root, "src", "assets", "shots"); const shots = path.join(root, "src", "assets", "shots");
const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
function requestedSlugs(argv) {
const slugs = new Set();
for (let index = 2; index < argv.length; index += 1) {
const argument = argv[index];
if (argument === "--slug") {
const slug = argv[index + 1];
if (!slug) throw new Error("--slug requires a showcase slug");
slugs.add(slug);
index += 1;
} else if (argument.startsWith("--slug=")) {
slugs.add(argument.slice("--slug=".length));
} else {
throw new Error(`Unknown argument: ${argument}`);
}
}
return slugs;
}
const CHROME = [ const CHROME = [
"C:/Program Files/Google/Chrome/Application/chrome.exe", "C:/Program Files/Google/Chrome/Application/chrome.exe",
"C:/Program Files (x86)/Google/Chrome/Application/chrome.exe", "C:/Program Files (x86)/Google/Chrome/Application/chrome.exe",
@ -44,6 +62,12 @@ const JOBS = [
}, },
]; ];
const requested = requestedSlugs(process.argv);
const known = new Set(JOBS.map((job) => job.slug));
const unknown = [...requested].filter((slug) => !known.has(slug));
if (unknown.length) throw new Error(`Unknown showcase slug(s): ${unknown.join(", ")}`);
const selectedJobs = requested.size ? JOBS.filter((job) => requested.has(job.slug)) : JOBS;
const browser = await puppeteer.launch({ const browser = await puppeteer.launch({
executablePath: CHROME, executablePath: CHROME,
headless: "new", headless: "new",
@ -56,7 +80,7 @@ const browser = await puppeteer.launch({
defaultViewport: { width: 1440, height: 900 }, defaultViewport: { width: 1440, height: 900 },
}); });
for (const job of JOBS) { for (const job of selectedJobs) {
const page = await browser.newPage(); const page = await browser.newPage();
await page.setCacheEnabled(false); await page.setCacheEnabled(false);
const file = path.join(root, "public/work", job.slug, "index.html"); const file = path.join(root, "public/work", job.slug, "index.html");

View file

@ -8,6 +8,24 @@ const rootDir = path.resolve(__dirname, "..");
const chromePath = "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"; const chromePath = "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe";
const shotsDir = path.join(rootDir, "src", "assets", "shots"); const shotsDir = path.join(rootDir, "src", "assets", "shots");
function requestedSlugs(argv) {
const slugs = new Set();
for (let index = 2; index < argv.length; index += 1) {
const argument = argv[index];
if (argument === "--slug") {
const slug = argv[index + 1];
if (!slug) throw new Error("--slug requires a showcase slug");
slugs.add(slug);
index += 1;
} else if (argument.startsWith("--slug=")) {
slugs.add(argument.slice("--slug=".length));
} else {
throw new Error(`Unknown argument: ${argument}`);
}
}
return slugs;
}
if (!fs.existsSync(shotsDir)) { if (!fs.existsSync(shotsDir)) {
fs.mkdirSync(shotsDir, { recursive: true }); fs.mkdirSync(shotsDir, { recursive: true });
} }
@ -94,6 +112,13 @@ const targets = [
}, },
]; ];
const requested = requestedSlugs(process.argv);
const slugOf = (target) => path.basename(path.dirname(target.url));
const known = new Set(targets.map(slugOf));
const unknown = [...requested].filter((slug) => !known.has(slug));
if (unknown.length) throw new Error(`Unknown showcase slug(s): ${unknown.join(", ")}`);
const selectedTargets = requested.size ? targets.filter((target) => requested.has(slugOf(target))) : targets;
console.log("📸 Capturing high-resolution screenshots..."); console.log("📸 Capturing high-resolution screenshots...");
const shot = (t, url, out) => { const shot = (t, url, out) => {
@ -111,7 +136,7 @@ const shot = (t, url, out) => {
]); ]);
}; };
for (const t of targets) { for (const t of selectedTargets) {
console.log(`- Capturing ${path.basename(t.out)}`); console.log(`- Capturing ${path.basename(t.out)}`);
try { try {
shot(t, t.url, t.out); shot(t, t.url, t.out);

View file

@ -10,6 +10,30 @@ const CHROME = "C:/Program Files/Google/Chrome/Application/chrome.exe";
const shots = path.join(root, "src", "assets", "shots"); const shots = path.join(root, "src", "assets", "shots");
const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
function requestedSlugs(argv) {
const slugs = new Set();
for (let index = 2; index < argv.length; index += 1) {
const argument = argv[index];
if (argument === "--slug") {
const slug = argv[index + 1];
if (!slug) throw new Error("--slug requires an app slug");
slugs.add(slug);
index += 1;
} else if (argument.startsWith("--slug=")) {
slugs.add(argument.slice("--slug=".length));
} else {
throw new Error(`Unknown argument: ${argument}`);
}
}
return slugs;
}
const requested = requestedSlugs(process.argv);
const known = new Set(["gaon-lms", "dure-enrollment"]);
const unknown = [...requested].filter((slug) => !known.has(slug));
if (unknown.length) throw new Error(`Unknown app slug(s): ${unknown.join(", ")}`);
const selected = requested.size ? requested : known;
const browser = await puppeteer.launch({ const browser = await puppeteer.launch({
executablePath: CHROME, executablePath: CHROME,
headless: "new", headless: "new",
@ -24,24 +48,28 @@ const open = async (app) => {
await sleep(250); await sleep(250);
}; };
// 가온: 대시보드 본컷 + 학적 카드(수강생 모달) -2컷 if (selected.has("gaon-lms")) {
await open("gaon-lms"); // 가온: 대시보드 본컷 + 학적 카드(수강생 모달) -2컷
await page.screenshot({ path: path.join(shots, "work-gaon-lms.png") }); await open("gaon-lms");
await page.screenshot({ path: path.join(root, "public/work/gaon-lms/og.png") }); await page.screenshot({ path: path.join(shots, "work-gaon-lms.png") });
await page.evaluate(() => document.querySelector('[data-view="students"]').click()); await page.screenshot({ path: path.join(root, "public/work/gaon-lms/og.png") });
await page.evaluate(() => document.querySelector("#stu-table [data-stu]").click()); await page.evaluate(() => document.querySelector('[data-view="students"]').click());
await page.waitForSelector("#stu-dialog[open]"); await page.evaluate(() => document.querySelector("#stu-table [data-stu]").click());
await sleep(350); // 모달 등장 모션 200ms + 여유 await page.waitForSelector("#stu-dialog[open]");
await page.screenshot({ path: path.join(shots, "work-gaon-lms-2.png") }); await sleep(350); // 모달 등장 모션 200ms + 여유
await page.screenshot({ path: path.join(shots, "work-gaon-lms-2.png") });
}
// 두레: 과목 찾기 본컷 + 강의계획(과목 모달) -2컷 if (selected.has("dure-enrollment")) {
await open("dure-enrollment"); // 두레: 과목 찾기 본컷 + 강의계획(과목 모달) -2컷
await page.screenshot({ path: path.join(shots, "work-dure-enrollment.png") }); await open("dure-enrollment");
await page.screenshot({ path: path.join(root, "public/work/dure-enrollment/og.png") }); await page.screenshot({ path: path.join(shots, "work-dure-enrollment.png") });
await page.evaluate(() => document.querySelector(".course-row").click()); await page.screenshot({ path: path.join(root, "public/work/dure-enrollment/og.png") });
await page.waitForSelector("#course-dialog[open]"); await page.evaluate(() => document.querySelector(".course-row").click());
await sleep(350); await page.waitForSelector("#course-dialog[open]");
await page.screenshot({ path: path.join(shots, "work-dure-enrollment-2.png") }); await sleep(350);
await page.screenshot({ path: path.join(shots, "work-dure-enrollment-2.png") });
}
await browser.close(); await browser.close();
console.log("work shots + og.png 갱신 완료"); console.log("work shots + og.png 갱신 완료");

View file

@ -170,6 +170,12 @@ const UTILS = `
for (const t of tops) if (!ls.length || t - ls[ls.length - 1] > 5) ls.push(t); for (const t of tops) if (!ls.length || t - ls[ls.length - 1] > 5) ls.push(t);
return ls.length; return ls.length;
}; };
window.__textLines = (sel) => {
const el = typeof sel === "string" ? document.querySelector(sel) : sel;
if (!el) return -1;
const text = [...el.childNodes].find((node) => node.nodeType === 3 && node.textContent.trim());
return window.__lines(text || el);
};
`; `;
const browser = await puppeteer.default.launch({ executablePath: CHROME, headless: "new", args: ["--force-device-scale-factor=1"] }); const browser = await puppeteer.default.launch({ executablePath: CHROME, headless: "new", args: ["--force-device-scale-factor=1"] });
@ -386,7 +392,7 @@ for (const p of CFG.pages) {
const bl = await page.evaluate((sel) => { const bl = await page.evaluate((sel) => {
const els = [...document.querySelectorAll(sel)]; const els = [...document.querySelectorAll(sel)];
const el = els.find((e) => e.offsetParent !== null || getComputedStyle(e).position === "fixed") || els[0]; const el = els.find((e) => e.offsetParent !== null || getComputedStyle(e).position === "fixed") || els[0];
return window.__lines(el); return window.__textLines(el);
}, p.brand); }, p.brand);
bl === 1 ? pass(`${p.name} 브랜드 1줄`) : fail(`${p.name} 브랜드 ${bl}줄(수축 의심)`); bl === 1 ? pass(`${p.name} 브랜드 1줄`) : fail(`${p.name} 브랜드 ${bl}줄(수축 의심)`);
} }

View file

@ -1,6 +1,6 @@
# E2E 시나리오 게이트 — 2026-08-24 19:47 # E2E 시나리오 게이트 — 2026-08-26 09:12
단언 1805 · 통과 1805 · 실패 0 — **GREEN — 전 단언 통과** 단언 1799 · 통과 1799 · 실패 0 — **GREEN — 전 단언 통과**
| 스위트 | 통과 | 실패 | | 스위트 | 통과 | 실패 |
|---|---|---| |---|---|---|
@ -10,7 +10,7 @@
| M320 매트릭스 | 90 | 0 | | M320 매트릭스 | 90 | 0 |
| M390 매트릭스 | 105 | 0 | | M390 매트릭스 | 105 | 0 |
| M1 터치타깃 | 231 | 0 | | M1 터치타깃 | 231 | 0 |
| M2 대비 | 1093 | 0 | | M2 대비 | 1087 | 0 |
| M768 매트릭스 | 90 | 0 | | M768 매트릭스 | 90 | 0 |
| S7 뒤로 가기 | 6 | 0 | | S7 뒤로 가기 | 6 | 0 |
| S7 뒤로 가기·모달 | 5 | 0 | | S7 뒤로 가기·모달 | 5 | 0 |

View file

@ -60,6 +60,12 @@ const UTILS = `
for (const t of tops) if (!ls.length || t - ls[ls.length - 1] > 5) ls.push(t); for (const t of tops) if (!ls.length || t - ls[ls.length - 1] > 5) ls.push(t);
return ls.length; return ls.length;
}; };
window.__textLines = (sel) => {
const el = typeof sel === "string" ? document.querySelector(sel) : sel;
if (!el) return -1;
const text = [...el.childNodes].find((node) => node.nodeType === 3 && node.textContent.trim());
return window.__lines(text || el);
};
window.__tapSweep = () => { window.__tapSweep = () => {
const EXEMPT_STRICT = ".undo-btn, .row-open"; const EXEMPT_STRICT = ".undo-btn, .row-open";
const out = []; const out = [];
@ -265,7 +271,7 @@ for (const [app, cfg] of Object.entries(APPS)) {
const h1 = await page.evaluate(() => window.__lines(document.querySelector(".view.is-on h2.v-h, h1"))); const h1 = await page.evaluate(() => window.__lines(document.querySelector(".view.is-on h2.v-h, h1")));
ok(h1 <= 1, S, `${app}/${v}@${w} 제목 1줄`, h1 + "줄"); ok(h1 <= 1, S, `${app}/${v}@${w} 제목 1줄`, h1 + "줄");
if (w === 390) { if (w === 390) {
const brand = await page.evaluate(() => window.__lines(document.querySelector(".brand, .appbar .brand"))); const brand = await page.evaluate(() => window.__textLines(document.querySelector(".brand, .appbar .brand")));
ok(brand <= 1, S, `${app}/${v}@390 브랜드 1줄`, brand + "줄"); ok(brand <= 1, S, `${app}/${v}@390 브랜드 1줄`, brand + "줄");
} }
const nav = await page.evaluate(() => { const nav = await page.evaluate(() => {

View file

@ -170,7 +170,8 @@ for (const [app, views] of [["gaon-lms", GAON_VIEWS], ["dure-enrollment", DURE_V
await page.setViewport({ width: 390, height: 844, isMobile: true, hasTouch: true }); await page.setViewport({ width: 390, height: 844, isMobile: true, hasTouch: true });
await openApp(page, app); await openApp(page, app);
const brandLines = await page.evaluate("window.__lines(document.querySelector('.brand, .appbar .brand'))"); // 브랜드 마크의 세로 중앙 정렬 차이는 줄바꿈이 아니다. 실제 브랜드 문구만 센다.
const brandLines = await page.evaluate("window.__textLines(document.querySelector('.brand, .appbar .brand'))");
if (brandLines > 1) fail(`${app} 브랜드 세로 쌓임`, brandLines + "줄"); if (brandLines > 1) fail(`${app} 브랜드 세로 쌓임`, brandLines + "줄");
else pass(`${app} 브랜드 1줄`); else pass(`${app} 브랜드 1줄`);
} }

View file

@ -0,0 +1,59 @@
// 배포 텍스트에는 한자를 두지 않는다. 이미지처럼 텍스트가 아닌 자산은 이 검사의 대상이 아니다.
import fs from "node:fs";
import path from "node:path";
import url from "node:url";
const here = path.dirname(url.fileURLToPath(import.meta.url));
const root = path.resolve(here, "..");
const TEXT_EXTENSIONS = new Set([".astro", ".css", ".html", ".js", ".json", ".mjs", ".svg", ".ts", ".tsx", ".txt", ".webmanifest", ".xml"]);
const HANJA = /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/u;
const HANJA_GLOBAL = /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/gu;
function scanRoots(argv) {
const roots = [];
for (let index = 2; index < argv.length; index += 1) {
const argument = argv[index];
const value = argument === "--root" ? argv[++index] : argument.startsWith("--root=") ? argument.slice("--root=".length) : null;
if (!value) throw new Error(`Unknown or incomplete argument: ${argument}`);
const candidate = path.resolve(root, value);
if (candidate !== root && !candidate.startsWith(root + path.sep)) throw new Error(`Root must stay inside apps/site: ${value}`);
roots.push(candidate);
}
return roots.length ? roots : [path.join(root, "src"), path.join(root, "public")];
}
function textFiles(directory) {
const files = [];
const walk = (current) => {
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
const candidate = path.join(current, entry.name);
if (entry.isDirectory()) walk(candidate);
else if (entry.isFile() && TEXT_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(candidate);
}
};
walk(directory);
return files;
}
const failures = [];
let scanned = 0;
for (const scanRoot of scanRoots(process.argv)) {
if (!fs.existsSync(scanRoot)) throw new Error(`Scan root does not exist: ${path.relative(root, scanRoot) || "."}`);
for (const file of textFiles(scanRoot)) {
scanned += 1;
const lines = fs.readFileSync(file, "utf8").split(/\r?\n/);
lines.forEach((line, index) => {
if (!HANJA.test(line)) return;
const characters = [...new Set(line.match(HANJA_GLOBAL) || [])].join("");
failures.push(`${path.relative(root, file).replaceAll("\\", "/")}:${index + 1} contains ${characters}`);
});
}
}
if (failures.length) {
console.error(`FAIL: ${failures.length} Hanja occurrence(s) across ${scanned} shipped text files.`);
for (const failure of failures) console.error(` ${failure}`);
process.exit(1);
}
console.log(`PASS: ${scanned} shipped text files contain no Hanja.`);

View file

@ -257,7 +257,7 @@ try {
// 4. 새벽청과 uses the current Korean market date and turns every price row into a readable lot. // 4. 새벽청과 uses the current Korean market date and turns every price row into a readable lot.
await goto(page, urls.market); await goto(page, urls.market);
const stamp = expectedKoreanStamp(); const stamp = expectedKoreanStamp();
const date = await page.$eval(".hero-stamp time", (time) => ({ text: time.textContent.trim(), datetime: time.dateTime })); const date = await page.$eval(".hero-meta time", (time) => ({ text: time.textContent.trim(), datetime: time.dateTime }));
assert(date.text === stamp.text && date.datetime === stamp.datetime, "새벽청과 date", "market stamp is current in Korea", `${date.text}/${date.datetime}`); assert(date.text === stamp.text && date.datetime === stamp.datetime, "새벽청과 date", "market stamp is current in Korea", `${date.text}/${date.datetime}`);
assert((await page.$$eval("[data-market-row]", (items) => items.length)) === 6, "새벽청과 lot board", "six selectable lots"); assert((await page.$$eval("[data-market-row]", (items) => items.length)) === 6, "새벽청과 lot board", "six selectable lots");
await page.click('[data-market-row][data-name="무화과"]'); await page.click('[data-market-row][data-name="무화과"]');

View file

@ -1,4 +1,4 @@
# TWENTYOZ E2E — 2026-08-24 19:48 # TWENTYOZ E2E — 2026-08-26 09:13
**148 assertions passed · GREEN** **148 assertions passed · GREEN**

View file

@ -1,21 +1,22 @@
# 검증 리포트 — 2026-08-24 19:48 # 검증 리포트 — 2026-08-26 09:13
게이트: **전 계층 통과 — 배포 가능** · 총 167.0초 게이트: **전 계층 통과 — 배포 가능** · 총 195.3초
| 계층 | 결과 | 항목 | 소요 | | 계층 | 결과 | 항목 | 소요 |
|---|---|---|---| |---|---|---|---|
| L0-자산/트웬티온스 3D | PASS | PASS — | 0.2s | | L0-자산/트웬티온스 3D | PASS | PASS — | 0.3s |
| L0-정적/stylelint | PASS | PASS — | 1.7s | | L0-한자 금지/배포 텍스트 | PASS | PASS 1 | 0.1s |
| L0-정식/html-validate | PASS | PASS — | 1.6s | | L0-정적/stylelint | PASS | PASS — | 2.4s |
| L0-무결성/변수·경로·조판 | PASS | PASS — | 0.2s | | L0-정식/html-validate | PASS | PASS — | 1.9s |
| L0-무결성/변수·경로·조판 | PASS | PASS — | 0.1s |
| L1-단위/계산로직 | PASS | PASS 40 | 0.1s | | L1-단위/계산로직 | PASS | PASS 40 | 0.1s |
| L2-기능/감사 | PASS | PASS 72 | 9.7s | | L2-기능/감사 | PASS | PASS 72 | 10.0s |
| L2-불변식/렌더 | PASS | PASS 10 | 16.6s | | L2-불변식/렌더 | PASS | PASS 10 | 36.9s |
| L2-디자인 게이트/메인 | PASS | PASS 130 | 32.6s | | L2-디자인 게이트/메인 | PASS | PASS 130 | 37.9s |
| L2-디자인 게이트/제품 | PASS | PASS 15 | 6.9s | | L2-디자인 게이트/제품 | PASS | PASS 15 | 7.2s |
| L3-시각회귀/diff | PASS | PASS 30 | 11.5s | | L3-시각회귀/diff | PASS | PASS 30 | 11.8s |
| L4-WebKit | PASS | PASS 8 | 3.6s | | L4-WebKit | PASS | PASS 8 | 3.8s |
| L5-탐색/키보드 | PASS | PASS 14 | 6.1s | | L5-탐색/키보드 | PASS | PASS 14 | 6.3s |
| L6-시나리오/E2E | PASS | PASS — | 42.6s | | L6-시나리오/E2E | PASS | PASS — | 43.6s |
| L6-시나리오/최근 쇼케이스 | PASS | PASS 115 | 11.0s | | L6-시나리오/최근 쇼케이스 | PASS | PASS 115 | 10.9s |
| L6-시나리오/트웬티온스 | PASS | PASS 148 | 22.7s | | L6-시나리오/트웬티온스 | PASS | PASS 148 | 22.0s |

View file

@ -15,6 +15,10 @@ const steps = [
layer: "L0-자산/트웬티온스 3D", layer: "L0-자산/트웬티온스 3D",
cmd: () => spawnSync(process.execPath, ["tools/build-showcase-assets.mjs", "--check"], { cwd: root, encoding: "utf8" }), cmd: () => spawnSync(process.execPath, ["tools/build-showcase-assets.mjs", "--check"], { cwd: root, encoding: "utf8" }),
}, },
{
layer: "L0-한자 금지/배포 텍스트",
cmd: () => spawnSync(process.execPath, ["tools/no-hanja.mjs"], { cwd: root, encoding: "utf8" }),
},
{ {
layer: "L0-정적/stylelint", layer: "L0-정적/stylelint",
cmd: () => spawnSync(npx, ["stylelint", "public/work/*/styles/*.css"], { cwd: root, encoding: "utf8", shell: true }), cmd: () => spawnSync(npx, ["stylelint", "public/work/*/styles/*.css"], { cwd: root, encoding: "utf8", shell: true }),

View file

@ -27,6 +27,19 @@ const UTILS = `
for (const t of tops) if (!ls.length || t - ls[ls.length - 1] > 5) ls.push(t); for (const t of tops) if (!ls.length || t - ls[ls.length - 1] > 5) ls.push(t);
return ls.length; return ls.length;
}; };
window.__textLines = (sel) => {
const el = document.querySelector(sel);
if (!el) return -1;
const text = [...el.childNodes].find((node) => node.nodeType === 3 && node.textContent.trim());
if (!text) return window.__lines(sel);
const range = document.createRange();
range.selectNodeContents(text);
const rects = [...range.getClientRects()].filter((r) => r.width > 1 && r.height > 4);
const tops = rects.map((r) => r.top).sort((a, b) => a - b);
const ls = [];
for (const t of tops) if (!ls.length || t - ls[ls.length - 1] > 5) ls.push(t);
return ls.length;
};
`; `;
const browser = await webkit.launch(); const browser = await webkit.launch();
@ -40,7 +53,7 @@ for (const [app, views] of Object.entries(APPS)) {
await page.evaluate(() => document.fonts.ready); await page.evaluate(() => document.fonts.ready);
// 브랜드 1줄 + h1 1줄 + 페이지 오버플로 // 브랜드 1줄 + h1 1줄 + 페이지 오버플로
const brand = await page.evaluate(`__lines('.brand')`); const brand = await page.evaluate(`__textLines('.brand')`);
if (brand > 1) fail(`${app} WebKit 브랜드 세로`, brand + "줄"); else pass(`${app} WebKit 브랜드`); if (brand > 1) fail(`${app} WebKit 브랜드 세로`, brand + "줄"); else pass(`${app} WebKit 브랜드`);
const h1 = await page.evaluate(`__lines('h1')`); const h1 = await page.evaluate(`__lines('h1')`);
if (h1 > 1) fail(`${app} WebKit h1 여러 줄`, h1 + "줄"); else pass(`${app} WebKit h1`); if (h1 > 1) fail(`${app} WebKit h1 여러 줄`, h1 + "줄"); else pass(`${app} WebKit h1`);

View file

@ -1,5 +1,11 @@
# designpaca # designpaca
## 0.9.2
### Patch Changes
- 컴퓨터 로케일을 기본 카피 언어로 고정하고, 영어 외 제3언어와 무관한 문화권 장식을 명시 요청 없이 생성하지 않도록 한다.
## 0.9.1 ## 0.9.1
### Patch Changes ### Patch Changes

View file

@ -1,6 +1,6 @@
{ {
"name": "designpaca", "name": "designpaca",
"version": "0.9.1", "version": "0.9.2",
"description": "웹 디자인 파이프라인 스킬 — Claude Code · Codex · Cursor 에 한 줄로 설치한다", "description": "웹 디자인 파이프라인 스킬 — Claude Code · Codex · Cursor 에 한 줄로 설치한다",
"keywords": [ "keywords": [
"design", "design",

View file

@ -1,5 +1,11 @@
# @designpaca/core # @designpaca/core
## 0.9.2
### Patch Changes
- 컴퓨터 로케일을 기본 카피 언어로 고정하고, 영어 외 제3언어와 무관한 문화권 장식을 명시 요청 없이 생성하지 않도록 한다.
## 0.9.1 ## 0.9.1
### Patch Changes ### Patch Changes

Some files were not shown because too many files have changed in this diff Show more