feat: strengthen design skill and refresh all showcases
Some checks failed
ci / build (push) Has been cancelled

This commit is contained in:
Yun Chan 2026-09-12 18:16:33 +09:00
parent 69e5cc1efd
commit e09d4efb1c
257 changed files with 8196 additions and 2374 deletions

View file

@ -0,0 +1,2 @@
# Preserve the upstream NAVER license pages verbatim; these are font provenance, not showcase routes.
public/work/*/assets/fonts/LICENSE.html

View file

@ -1159,6 +1159,83 @@ Pretendard `dynamic-subset` 은 유니코드 범위별로 파일을 쪼개고
- 갤러리 카드 네 장과 공유 이미지 두 장은 확정 화면을 실제 Chrome으로 다시 캡처했다.
- 구현 중 발견한 21개 교훈은 `research/showcase-duo-learning-ledger-2026-08-29.md`에 근거와 함께 남겼고, 재사용 가능한 규칙은 `packages/skill/references/trustworthy-showcases.md`와 관련 게이트 문서에 반영했다.
## 전 쇼케이스 반응형·여백 재감사 (2026-08-30)
### 연장 범위와 유지한 방향
이 판은 새 콘셉트로 갈아엎지 않는 **연장 경로**다. 21개 showcase의 서로 다른 프리셋과 실제/생성 이미지, 정보 구조는 유지하고, 화면이 커지거나 작아질 때 그 의도가 무너지는 지점만 고쳤다. 위의 "기존 16종에 남아 있는 것"은 이 항목으로 대체된다.
- **본문과 정보 표면을 분리했다.** 1920·2560px에서 문장·폼은 기존 `measure`를 지키고, CFD 사양·시세·원장·관리 작업대·히어로처럼 정보 밀도가 필요한 표면만 named wide container로 넓혔다. 전역 `max-width`를 풀어 빈 여백을 만든 곳은 없다.
- **가시 내용의 경계를 고쳤다.** 버튼·검색·select·저장 동작·모바일 프리셋·지도 핀의 hitbox와 실제 텍스트에 여백을 주고, 첫/마지막 table/flex 셀의 0px padding을 제거했다. 입력 상자의 실제 겹침도 DOM rect로 검사한다.
- **읽기와 접근성을 함께 고쳤다.** 저대비 보조 토큰을 WCAG AA에 맞게 조정하고, `transition: all`을 대상 속성으로 분리했다. 갤러리의 클릭 div는 overlay button으로 바꾸고, 표 header `scope`, skip link, input label/name/autocomplete, `prefers-reduced-motion`, icon button 이름, 이미지 intrinsic size를 보완했다.
- **가상 흐름의 진실 범위를 가깝게 표시했다.** 청온·카스크·마테리아의 견적/예약/갤러리 예시는 행동 근처에서 예시·저장/전송 없음 범위를 알리고, 만들어 낸 연락처·기본 개인정보·가짜 평점은 제거했다.
### 감사기와 기준 화면
`tools/showcase-audit.mjs`는 기본 폭을 `320, 390, 768, 1440, 1920, 2560`으로 확장했다. 실제 렌더에서 가로 overflow, 대비, h1, 컨트롤 줄바꿈뿐 아니라 다음을 hard fail로 잰다.
- 버튼 라벨·field value·표/ARIA cell의 가시 좌우 inset(기본 8 CSS px)
- visible input/select/textarea의 면적 겹침
- native/ARIA table cell의 실제 글자와 셀 테두리 사이 여백
CSS `color-mix()`가 Chromium에서 반환하는 CSS Color 4 `color(srgb …)`와 반투명 background 합성을 파서에 반영했다. 또 sr-only/aria-hidden 장식/빈 overlay를 `Range.selectNodeContents()`로 가시 텍스트에 섞어 여백 실패를 만들던 측정 오류를 text node 단위로 고쳤다. 테스트 도구의 false RED를 UI 수정으로 덮지 않는다.
가온의 세 시각 기준은 44px 알림 hitbox와 캘린더·요약 표의 좌우 여백이라는 의도 변경을 실제 current/baseline 캡처로 비교한 뒤에만 갱신했다. `tools/visual.mjs --update-baseline=<이름,...>`로 대상 기준만 갱신할 수 있게 하여 다른 기준 화면을 재촬영으로 덮지 않는다.
### 6단계 증거
- 전 쇼케이스 정적 실렌더: **26개 HTML × 6개 폭 = 156/156 PASS**
- 저장소 전체 `npm run verify`: L0~L6 **전 계층 PASS**. 여기에는 실렌더 156, 메인 디자인 게이트 178, 제품 게이트 15, 시각 회귀 30, WebKit 8, 최근 쇼케이스 E2E 115, TWENTYOZ E2E 148, 쇼케이스 듀오 90 scenario / 350 assertion / 12 capture가 포함된다.
- `npm run build`: 성공. 배포 텍스트 한자 검사도 **98개 파일 PASS**.
- 21개 카드의 현재 화면/보조 화면과 듀오 OG를 실제 Chromium으로 다시 캡처했다.
### 재사용 규칙 승격
문헌 근거와 이번의 RED→GREEN 증거는 designpaca skill의 `references/layout.md`, `references/preflight.md`, `references/audit-gate.md`에 반영했다. 초광폭 레이아웃을 별도 상태로 다루고, WCAG의 최소 target size와 프로젝트의 시각 여백 기본값을 구분하며, 반응형 표·폼·진실 라벨·측정기 정확도를 함께 게이트로 유지한다.
## 정적 쇼케이스 재구성 · 모달 위치 · 초광폭 이미지 계약 (2026-08-30)
### 이번 판의 판단
최근의 상태형 작업물(목요일 화원, 마테리아, 한남자리, 철판클럽, RFS, 새벽청과, TWENTYOZ)은 이미 각 도메인의 정보 구조와 상호작용이 충분히 구분되어 있어 새 문법으로 덮지 않았다. 대신 그 안의 팝업 위치·짧은 화면·초광폭 수축 결함만 고쳤다. 오래된 정적 쇼케이스는 다음 10개를 실제 정보 표면과 첫 화면의 판단 근거가 보이도록 재구성했다.
- `baekje-celadon`, `font-foundry`, `form-architecture`, `hyang-incense`: 전시·활자·도면·기록이 첫 시야에서 행동과 함께 읽히도록 재배치
- `lumina-optics`, `orbit-aerospace`, `pulsegate`, `synapse-bci`: 빛/비행/접근/임상이라는 주장에 대응하는 실제 시험 조건·표본·한계를 판독 레지스터로 고정
- `muwol-coffee`, `overtone-sound`: 장식적 영웅면 대신 로스터의 좌표/원장과 설치 아카이브의 조건을 정보 밀도로 구성
### 사용자 보고에서 고정한 계약
- 마테리아의 native dialog는 전역 reset이 UA `margin`을 지워 좌상단에 붙었던 결함이었다. 프로젝트 modal과 한남 듀플렉스 gallery lightbox에 `fixed + inset:0 + margin:auto`와 viewport 상한·내부 scroll owner를 명시했다. 390·1440·2560에서 중심 오차 0~0.008px, 배경 scroll lock을 자동 단언한다.
- 가온의 알림 메뉴, 한남자리 공개 메뉴와 관리자 조사관은 390×320에서 화면 밖/0px scroll 상태가 되지 않도록 viewport-bound와 short-height 구성을 분리했다.
- 철판클럽은 2560에서 hero `1680px`, copy `688px`, media `856.7px`, 다음 content rail `1400px`으로 고정했다. 사진을 화면 반까지 무한 확장하지 않으며, `stage-wide/media-wide/copy-wide/workbench-wide`를 구분한다.
### 재발 방지와 증거
`tools/audit.mjs`가 마테리아 두 modal의 중심/scroll lock을 390·1440·2560에서, 한남자리 짧은 화면 scroll owner와 가온 알림 메뉴를 390×320에서, 철판클럽 hero/media/content rail을 2560에서 hard fail로 검사한다. 이 규칙은 공통 스킬의 `layout.md`, `preflight.md`, `audit-gate.md`에 승격했다. 모달의 포커스·키보드·닫기 계약은 [WAI-ARIA APG](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)를 근거로 한다.
### 공개 캐시 확인
정적 CSS의 stable URL은 HTML만 새로 배포해도 사용자 브라우저에 이전 스타일을 남길 수 있다. 이번 수정의 `main/site/tokens/franchise/landing/admin` 링크와 CSS `@import` 버전을 함께 올렸다. 공개 검증은 HTTP 200이 아니라 새 stylesheet href와 새 토큰·핵심 computed value가 실제로 로드됐는지까지 확인한다.
### 후속 정착 프레임 캡처·초광폭 보정 (2026-08-30)
- 모든 쇼케이스 카드·OG 캡처 경로를 `tools/capture-stable.mjs`로 통합했다. `file://` 즉시 스크린샷 대신 no-store 로컬 HTTP에서 폰트·이미지 decode·페이지별 bounded settle을 기다린 뒤, reduced-motion 환경과 video/audio·WAAPI·rAF 정지로 완성된 한 프레임을 저장한다. local 4xx, pageerror, 깨진 이미지는 결과물 생성 실패로 취급한다.
- 철판클럽의 2560px 히어로는 기존 1680px cap이 실제로 지나치게 좁아 양쪽에 맹탕을 만들었다. 히어로만 `2160px`, 카피 `900px`, 미디어 `1120px`으로 분리하고 메뉴/본문 레일은 `1400~1480px`에 남겼다. 넓은 화면은 하나의 전역 상한이 아니라, 시선의 중심이 되는 히어로와 읽기/정보 표면이 각각 다른 폭을 소유한다.
- 이미지 생성은 제공된 실제 사진을 대체하지 않는다. 브랜드가 요구하는 정확한 장면·광학적 분위기 같은 시각 자산이 실제로 비어 있고, 생성물·연출 라벨을 함께 둘 수 있을 때만 이미지 바이블의 슬롯별로 생성한다. 이번 두 결함은 각각 레이아웃과 WebGL 역할의 문제라서 생성 이미지로 덮지 않는다.
### 사용자 보고로 보강한 과정 분할 계약 (2026-08-30)
- 철판클럽의 `process`는 hero와 별도의 텍스트/이미지 분할 표면이다. 기존 모바일/중간 폭의 `padding-left: calc((100vw - --wrap) / 2)`가 초광폭 규칙에 남아 2560px에서 실제 제목·설명 너비를 **12px**로 만들었다. hero만 통과한 것은 이 결함의 증거가 아니다.
- 초광폭 과정 표면은 `--stage-wide` 1680px 안에서 copy **822px**, media **856px**로 제한하고 양쪽 logical padding을 함께 재설정했다. 제목은 의도한 2줄, 목록 문장은 읽는 폭으로 돌아왔으며 사진은 화면 절반까지 팽창하지 않는다.
- `tools/audit.mjs`는 hero뿐 아니라 이처럼 DOM/여백 규칙이 다른 각 text-media split의 rail, copy/media 폭, 실제 제목 폭과 줄 수를 2560px에서 hard fail로 잠근다. 재사용 스킬의 preflight·layout·audit 규칙도 같은 원리로 보강했다.
### 사용자 코멘트로 정정한 제품 카피와 이미지 계약 (2026-08-30)
- 가상 흐름이라도 사용자가 하려는 일이 아닌 제작 목적을 문구로 노출하지 않는다. `화면 동작용`, `쇼케이스`, `저장`처럼 구현을 설명하는 말은 `실제 주문·결제·저장을 제공하지 않습니다`처럼 행동과 한계를 함께 이해할 수 있는 말로 바꾼다.
- 조합 결과에 영속 동작이 없으면 저장 버튼이나 성공 상태를 만들지 않는다. 다음 행동은 실제로 가능한 범위 안에서 가상 매장 안내처럼 목적이 분명한 경로로 연결한다.
- CSS 조각으로 제품 자체를 흉내 내지 않는다. 철판클럽의 싱글 버거는 전용 4:3 생성 이미지(`assets/single-smash-generated.webp`, `960×720`)로 바꾸고 카드 안에 `생성 이미지` 라벨을 붙였다. 제품 사진은 제품을 고르는 근거이고, DOM 도식은 비교·설명에만 쓴다.
- 조합기처럼 반복되는 fieldset은 경계선만으로 묶지 않는다. 다음 fieldset 앞에 16px의 명시적 간격을 두고 1217px 실화면에서 legend와 이전 선택지가 붙어 보이지 않는지 확인한다.
## 홍보 사이트 전면 재설계 — Quiet Editorial Proof Surface (2026-08-30)
### 문제와 방향
@ -1214,3 +1291,15 @@ Pretendard `dynamic-subset` 은 유니코드 범위별로 파일을 쪼개고
- `2026-08-30` · 국소 — 히어로 아래 방향 결정·토큰 조판·검증 프레임과 7단계 진행 레일도 각각의 named CSS view timeline에 연결한다. 본문과 수치는 움직이지 않고 방향선·글립·토큰 표본·검사 화살표·짧은 진행선만 transform/opacity로 보간하며, 검증 프레임의 가장 넓은 판은 두 작은 판을 맞추는 고정 기준면으로 남긴다. 모바일 검증 완료 지점은 sticky GNB 아래 제목 여유 24px 이상에서 끝나도록 앞당기고, 미지원 브라우저·감소 모션·이펙트 제거 상태는 정적 완료 화면을 사용한다. KO/EN 전진·역스크롤 geometry 8개, 320~2560px 반응형, effects-off와 감소 모션을 80/80 시나리오·1000/1000 단언으로 고정한다.
- `2026-08-30` · 국소 — 사용자 체감 실패를 반영해 방향 결정·토큰과 조판의 timeline subject를 article에서 실제 `.direction-stage`·`.type-stage`로 내리고, 방향 면·벡터·세 노드와 글립·세 토큰 표본을 각 자식의 첫 가시 시점 이후에 서로 다른 범위로 조립한다. 글자 대비는 opacity로 낮추지 않으며 미지원·감소 모션·이펙트 제거 상태는 정적 완료 화면이다. KO/EN × 390/1440에서 110px 실제 wheel 전진·역진, offscreen 진행 상한, 가시 구간 dwell, 독립 위상과 token stagger를 새 E2E 계약으로 추가했고 전체 85/85 시나리오·1094/1094 단언을 통과했다.
- `2026-08-30` · 사용자 실화면 정정 — Direction의 한 줄짜리 독립 변형을 폐기하고, 정확한 세 점 좌표를 공유하는 두 SVG 선분으로 나눠 시작점을 고정한 채 스크롤로 길이만 드러낸다. 노드는 이동하지 않고 앞 선분이 좌표에 닿은 뒤 같은 중심에서 나타나며, 모바일도 `xMidYMid meet`로 비율을 보존한다. 마지막 선과 점은 `cover 80% / 86%` 안에 끝내 sticky GNB 아래에서 완성 상태를 읽을 시간을 남긴다. Tokens는 글립·기준선·세 역할 표본, Verification은 두 가변 프레임·네 검사 화살표, Process는 작은 행 장식 대신 목록 전체 높이의 연속 진행선을 각 named view timeline으로 보간한다. 실제 앱 브라우저 1316×1272에서 110px 전진·역진과 Direction endpoint 연결을 확인했고, KO/EN 320~2560px 전체 게이트는 86/86 시나리오·1122/1122 단언·8/8 캡처로 통과했다.
### 디자인 전문성 근거와 홍보 카피 정렬 (2026-09-12)
- Quiet Editorial Proof Surface의 밝은 Wanted Sans·신호 적색·이미지 없는 산출물 표본 세 개와 히어로의 양방향 스크롤 계약은 그대로 둔다. 고객 성과처럼 읽힐 수 있는 프로젝트 캡처를 홈에 되돌리지 않으며, 실제 사이트는 낮은 위계의 `/showcase/` 또는 `/en/showcase/` 링크로 직접 찾는다.
- 히어로는 웹 디자인 원리와 실제 사례 조사, 브랜드 맥락에 맞는 방향 결정, 반응형 구현, 실화면 검증을 한 제품 흐름으로 설명한다. 이 순서는 [NN/g의 사용성 휴리스틱](https://www.nngroup.com/articles/ten-usability-heuristics/)으로 확인하는 보편 기준과, 프로젝트 목적에 맞춰 선택하는 시각 스타일을 구분하는 근거를 드러낸다.
- 검증 근거는 수나 전환 성과로 포장하지 않는다. `7`은 0~6 파이프라인이고, 나머지 두 사실은 근거·맥락의 분리와 기능 검사/실화면 검토의 분리다. [Webby Awards 심사 기준](https://www.webbyawards.com/judging-criteria/)의 콘텐츠·구조와 내비게이션·시각 디자인·기능성·상호작용·전반 경험 구분도 화면의 인상만으로 품질을 단정하지 않는 보조 근거로 쓴다.
- 세부 검사에는 320px, 대비, 키보드, 감소 모션, 200% 확대, 실제 전후 화면의 구성·위계·반응형 동작, 출처 연결을 포함한다. 기존의 토큰·SEO 검사는 유지하되, 프로젝트에 따라 달라지는 검증 범위를 단일 하드 게이트 수로 홍보하지 않는다.
- 보존한 계약: 실제 고객 성과 주장 없음, 홈 산출물에는 이미지 없음, 히어로 그래픽과 모션은 변경하지 않음, 한국어·영어는 같은 정보 구조와 목적지 링크를 가짐. 빌드한 사이트 E2E는 86개 시나리오·1,122개 단언·8개 캡처를 통과했고, 개발 서버의 별도 증거는 KO/EN × 320·390·768·1440·1920·2560에서 overflow 0, 새 링크 12/12의 44px 이상 hit area와 키보드 목적지 2/2를 확인했다. 실제 클립보드 권한은 이 headless 환경에서 거절되어 두 언어 모두 선택 복사 안내와 전체 명령 선택으로 안전하게 전환됨을 확인했다. 새 캡처와 JSON은 `outputs/design-expertise-20260912/home/`에 보관한다.
### 확대 텍스트 표본 보정 (2026-09-12)
- 홈의 Direction 표본 문장은 절대 배치와 `overflow: clip` 조합에서 200% 확대 시 잘릴 수 있었다. 문장을 `position: relative`의 normal flow로 옮기고 기존 왼쪽 48%·너비를 유지했다. stage는 `display: flow-root`로 첫 자식 margin의 외부 collapse를 막는다. 기본 2:1 표본은 stage width 기준 `margin-top: 24%`, `margin-bottom: 5%`로 기존 중앙 위치를 보존하며, 긴 문장은 stage 높이를 자연스럽게 늘린다. 모바일 16:9에서는 `margin-top: 27%`, 28rem 이하에서는 기존 왼쪽 39%·너비 55%를 유지한다.

View file

@ -15,7 +15,7 @@
"report",
"notice"
],
"h1": "h2.v-h",
"h1": ".view.is-on h2.v-h",
"brand": ".brand"
},
{
@ -30,7 +30,7 @@
"archive",
"notices"
],
"h1": "h2.v-h",
"h1": ".view.is-on h2.v-h",
"brand": ".brand"
},
{

View file

@ -11,6 +11,7 @@
"test:e2e:recent-showcases": "node tools/recent-showcases-e2e.mjs",
"test:e2e:twentyoz": "node tools/twentyoz-e2e.mjs",
"test:e2e:showcase-duo": "node tools/showcase-duo-e2e.mjs",
"capture:showcase": "node tools/capture-shots.mjs && node tools/capture-work-shots.mjs && node tools/capture-new-shots.mjs && node tools/capture-showcase-duo.mjs",
"capture:showcase-duo": "node tools/capture-showcase-duo.mjs",
"capture:site-og": "node tools/capture-site-og.mjs",
"prebuild": "npm run build:showcase-assets",

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,13 @@
# MaruBuri source record
Downloaded 2026-09-12 from the official NAVER Hangeul static host. The files are retained unchanged: no conversion or subsetting was performed.
| File | Official URL | HTTP content type | Size | SHA-256 |
| --- | --- | --- | --- | --- |
| `MaruBuri-Regular.woff2` | https://hangeul.pstatic.net/hangeul_static/webfont/MaruBuri/MaruBuri-Regular.woff2 | `font/woff2` | 433776 bytes | `4CF1341CF2F23FB3E263712DFDE1D8F25EEDCC328B696A2E5A2C8ADD55E5C17B` |
| `MaruBuri-SemiBold.woff2` | https://hangeul.pstatic.net/hangeul_static/webfont/MaruBuri/MaruBuri-SemiBold.woff2 | `font/woff2` | 466108 bytes | `7AEF4862FA3219B0A7DEB6746E27116A38C4069F4424EB51EDF103B29795F5C2` |
- Official stylesheet checked: https://hangeul.pstatic.net/hangeul_static/css/maru-buri.css
- Official NAVER license page saved verbatim as `LICENSE.html`: https://help.naver.com/service/11029/contents/18088?lang=ko&osType=PC
- `LICENSE.html` SHA-256: `8538F7921744D5E040F603BC301465EFBD038F1054D0754B7508DE3286452EC3`
- The saved official page includes the NAVER copyright notice and the SIL Open Font License 1.1 text for MaruBuri.

View file

@ -0,0 +1,17 @@
# 비색의 침묵
암실의 청자 사진을 주연으로 유지하고 회색 카드나 격자는 추가하지 않는다. 제1·2실의 방 번호, 제목, 재질 설명, 매크로 캡션을 공통 레일로 정리했다. 첫 사진은 기존 4:5 비율을 보존하고 관람 구성 CTA와 가상 관람 안내를 인접하게 둔다.
| 판단 축 | 적용 | 한계 |
| --- | --- | --- |
| 구도 | Hofmann의 레일과 비대칭을 참고해 방 정보와 캡션을 같은 열에 맞췄다. | 전시 사진의 감상 효과를 보편적으로 예측하지 않는다. |
| 타입 | Ruder를 참고해 방 번호와 제목·설명의 크기 차이를 명확히 했다. | 서체만으로 체류나 이해가 바뀐다고 단정하지 않는다. |
| 색 | Albers를 참고해 어두운 바탕, 비색 강조, 밝은 CTA의 역할을 나눴다. | 실제 명도 대비는 브라우저 검증에서 확인한다. |
토큰은 `--surface`, `--ink`, `--accent`, `--container`, `--space-*`를 유지한다. 로컬 MaruBuri 400/600은 `styles/fonts.css`, Pretendard는 기존 CDN을 유지한다. 가상 전시·관람 안내와 기존 앵커는 보존했다.
## 2026-09-12 현재 검증 기록
- `verify-final` 18개 묶음은 16개가 즉시 통과했다. 전체 묶음의 invariant·visual 검사는 Gaon·Dure를 대상으로 하며, 나머지 2개는 수정 뒤 release 독립 재실행에서 ALL PASS·ALL MATCH였다.
- root는 28개 모바일, 21개 데스크톱, 7개 하위 경로와 30개 앱 상태를 직접 검토해 수용했다. stress 112건은 hard failure 0이며, 가로 넘침 4건은 targeted fix 뒤 390px 재측정에서 0이다.
- 이 기록은 렌더·계약 범위의 현재 결과다. 모든 폭·키보드 경로·인쇄 등 개별 기능을 완전 재실행했다는 뜻이 아니며, 참여자 과업 효과도 미측정이다.

View file

@ -3,15 +3,15 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>비색의 침묵 · 고려청자 특별전</title>
<title>비색의 침묵 · 고려청자 전시 구성</title>
<meta
name="description"
content="천 년의 비취색, 흙과 불이 빚은 침묵의 곡선. 고려청자 유약의 빙렬과 상감을 세 개의 방에서 감상하는 특별전. 2026년 가을, 서울."
content="고려청자의 빙렬과 상감을 세 개의 방으로 읽는 가상 전시 구성입니다. 일정·장소·예약 정보는 예시입니다."
/>
<meta property="og:title" content="비색의 침묵 · 고려청자 특별전" />
<meta property="og:title" content="비색의 침묵 · 고려청자 전시 구성" />
<meta
property="og:description"
content="유약의 빙렬과 상감 문양을 세 개의 방에서 감상하는 특별전."
content="유약의 빙렬과 상감 문양을 세 개의 방으로 읽는 가상 전시 구성."
/>
<meta property="og:type" content="website" />
<meta name="theme-color" content="#121514" />
@ -19,13 +19,13 @@
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/webfontworld/naver/MaruBuri.css"
href="./styles/fonts.css?v=20260912all"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css"
/>
<link rel="stylesheet" href="./styles/main.css?v=2" />
<link rel="stylesheet" href="./styles/main.css?v=20260912all" />
<link
rel="icon"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23121514'/%3E%3Cpath d='M11 6h10c-1 3-1 5 0 7 2 3 3 5 3 8 0 4-3 7-8 7s-8-3-8-7c0-3 1-5 3-8 1-2 1-4 0-7z' fill='none' stroke='%236FA08F' stroke-width='1.5'/%3E%3C/svg%3E"
@ -47,10 +47,6 @@
</header>
<main id="main">
<!--
첫 방 — 암실. 전시 포스터가 아니라 첫 유물 하나.
명암이 곧 구조다: 사진이 어두울수록 유약이 가까워진다.
-->
<section class="hero" aria-labelledby="hero-h">
<div class="wrap hero-grid">
<figure class="hero-figure reveal">
@ -63,13 +59,13 @@
fetchpriority="high"
/>
<figcaption>
<span>청자 상감운학문 주자 · 12세기</span>
<span>호암미술관 소장</span>
<span>청자 주자 · 전시 연출 이미지</span>
<span>가상 전시 소품</span>
</figcaption>
</figure>
<div class="hero-text">
<p class="hero-kicker reveal" style="--delay: 100ms">
특별전 · 2026 가을
가상 전시 구성 · 고려청자
</p>
<h1 id="hero-h" class="reveal" style="--delay: 160ms">
천 년의 비색은<br />말이 없다
@ -80,17 +76,13 @@
않고 세 개의 암실에 놓아 두었습니다.
</p>
<div class="hero-actions reveal" style="--delay: 300ms">
<a class="btn" href="#visit">도슨트 예약</a>
<p class="hero-note">9월 5일 – 11월 22일 · 국립중앙박물관 기획전시실</p>
<a class="btn" href="#visit">관람 구성 보기</a>
<p class="hero-note">일정·장소·예약 정보는 전시 구성 예시이며 실제 관람을 접수하지 않습니다.</p>
</div>
</div>
</div>
</section>
<!--
첫 번째 방 — 빙렬. 유약이 식으며 스스로 그은 그림.
풀블리드 판: 페이지에서 가장 큰 면적을 질감에 내어준다.
-->
<section id="room-1" class="section plate" aria-labelledby="room1-h">
<div class="wrap plate-head">
<p class="room-no reveal">제1실</p>
@ -110,15 +102,11 @@
loading="lazy"
/>
<figcaption class="wrap">
빙렬 확대 · 12세기 청자 표면 · 측광
<span>빙렬 확대 · 12세기 청자 표면 · 측광</span>
</figcaption>
</figure>
</section>
<!--
두 번째 방 — 상감. 새기고 채우는 글자 없는 문장.
사진은 절반만. 나머지 절반은 문양의 문법을 텍스트로.
-->
<section id="room-2" class="section" aria-labelledby="room2-h">
<div class="wrap two">
<div class="two-text">
@ -151,35 +139,32 @@
</div>
</section>
<!--
관람 — 예약과 안내. 마지막 방은 서류다.
-->
<section id="visit" class="section band" aria-labelledby="visit-h">
<div class="wrap visit-grid">
<div>
<p class="room-no reveal">안내</p>
<h2 id="visit-h" class="reveal" style="--delay: 60ms">세 개의 방, 예약제</h2>
<h2 id="visit-h" class="reveal" style="--delay: 60ms">세 개의 방으로 보는 전시 구성</h2>
<p class="reveal" style="--delay: 120ms">
암실 조명 때문에 회당 인원을 30명으로 제한합니다. 도슨트는 하루 세 번,
유약과 가마 이야기만 합니다.
조명과 동선이 관람 경험을 어떻게 바꾸는지 보여 주기 위한 예시입니다.
아래 일정과 해설 방식은 실제 전시 운영 정보가 아닙니다.
</p>
</div>
<dl class="visit-dl reveal" style="--delay: 160ms">
<div>
<dt>기간</dt>
<dt>전시 기간 예시</dt>
<dd>2026년 9월 5일 – 11월 22일 · 월요일 휴관</dd>
</div>
<div>
<dt>시간</dt>
<dt>관람 시간 예시</dt>
<dd>10:00–18:00 · 금·토 21:00까지</dd>
</div>
<div>
<dt>도슨트</dt>
<dt>해설 구성 예시</dt>
<dd>11:00 / 14:00 / 16:00 · 회당 40분</dd>
</div>
<div>
<dt>예약</dt>
<dd><a class="link" href="#visit">국립중앙박물관 예약 시스템</a></dd>
<dd>실제 예약 시스템과 연결되지 않습니다.</dd>
</div>
</dl>
</div>
@ -188,23 +173,10 @@
<footer class="site-foot">
<div class="wrap foot-inner">
<p>비색의 침묵 · 고려청자 특별전</p>
<p class="muted">국립중앙박물관 기획전시실 · 서울 용산</p>
<p>비색의 침묵 · 고려청자 전시 구성</p>
<p class="muted">가상 전시 프로젝트 · 장소와 소장 정보는 예시입니다.</p>
</div>
</footer>
<script>
const io = new IntersectionObserver(
(entries) => {
for (const e of entries) {
if (!e.isIntersecting) continue;
e.target.classList.add("is-in");
io.unobserve(e.target);
}
},
{ rootMargin: "0px 0px -8% 0px", threshold: 0.05 },
);
for (const el of document.querySelectorAll(".reveal")) io.observe(el);
</script>
</body>
</html>

View file

@ -0,0 +1,15 @@
@font-face {
font-family: "MaruBuri";
src: url("../assets/fonts/MaruBuri-Regular.woff2") format("woff2");
font-style: normal;
font-weight: 400;
font-display: swap;
}
@font-face {
font-family: "MaruBuri";
src: url("../assets/fonts/MaruBuri-SemiBold.woff2") format("woff2");
font-style: normal;
font-weight: 600;
font-display: swap;
}

View file

@ -1,4 +1,4 @@
@import url("./tokens.css");
@import url("./tokens.css?v=20260912all");
/* ==========================================================================
비색의 침묵 — editorial-archival. 암실의 세 개의 방.
@ -132,6 +132,7 @@ a {
}
.nav {
display: flex;
flex-wrap: wrap;
gap: clamp(var(--space-3), 2vw, var(--space-5));
font-size: var(--step--1);
}
@ -151,13 +152,25 @@ a {
/* ── 첫 방(히어로) ────────────────────────────────────────────────────── */
.hero {
padding-block: var(--space-7);
display: grid;
align-items: center;
min-block-size: min(46rem, calc(100svh - 4rem));
padding-block: var(--space-6);
border-bottom: 1px solid var(--line);
}
.hero-grid {
display: grid;
grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
gap: var(--space-6) var(--space-7);
align-items: end;
grid-template-columns: minmax(15rem, 34rem) minmax(0, 1fr);
gap: clamp(var(--space-5), 6vw, var(--space-8));
align-items: center;
}
.hero-grid > *,
.two > *,
.visit-grid > * {
min-width: 0;
}
.hero-figure {
width: min(100%, 34rem);
}
.hero-figure img {
display: block;
@ -179,6 +192,10 @@ a {
letter-spacing: 0.12em;
margin-bottom: var(--space-4);
}
.hero-text {
max-width: 39rem;
padding-block: var(--space-4);
}
.hero-text .lead {
margin-top: var(--space-4);
}
@ -194,7 +211,10 @@ a {
color: var(--ink-faint);
}
.btn {
display: inline-block;
display: inline-flex;
align-items: center;
min-height: 44px;
padding-inline: var(--space-4);
background: var(--ink);
color: var(--surface);
font-weight: 500;
@ -221,6 +241,9 @@ a {
.section {
padding-block: var(--space-8);
}
.section[id] {
scroll-margin-top: 6rem;
}
.band {
background: var(--surface-raised);
border-block: 1px solid var(--line);
@ -229,16 +252,27 @@ a {
/* ── 제1실 — 풀블리드 판 ──────────────────────────────────────────────── */
.plate-head {
max-width: 40em;
display: grid;
grid-template-columns: minmax(8rem, 1fr) minmax(0, 5fr);
column-gap: var(--space-5);
max-width: var(--container);
}
.plate-head .room-no {
grid-column: 1;
grid-row: 1 / span 2;
margin: 0;
}
.plate-head h2 {
grid-column: 2;
margin-top: var(--space-1);
}
.plate-head .lead {
grid-column: 2;
margin-top: var(--space-4);
}
.plate-figure {
margin-top: var(--space-6);
width: min(100% - 2 * var(--pad-inline), var(--container-wide));
margin: var(--space-6) auto 0;
}
.plate-figure img {
display: block;
@ -246,10 +280,16 @@ a {
height: auto;
}
.plate-figure figcaption {
display: grid;
grid-template-columns: minmax(8rem, 1fr) minmax(0, 5fr);
column-gap: var(--space-5);
font-size: var(--step--1);
color: var(--ink-faint);
margin-top: var(--space-2);
}
.plate-figure figcaption span {
grid-column: 2;
}
/* ── 제2실 — 2단 ──────────────────────────────────────────────────────── */
@ -317,22 +357,22 @@ a {
/* ── 등장 모션 ────────────────────────────────────────────────────────── */
.reveal {
opacity: 0;
translate: 0 14px;
transition: opacity var(--dur-normal) var(--ease-soft),
translate var(--dur-normal) var(--ease-soft);
transition-delay: var(--delay, 0ms);
}
.reveal.is-in {
opacity: 1;
translate: 0 0;
}
@media (prefers-reduced-motion: reduce) {
.reveal {
opacity: 1;
translate: 0 0;
transition: none;
}
.two-text {
display: grid;
grid-template-columns: minmax(8rem, 1fr) minmax(0, 5fr);
column-gap: var(--space-5);
}
.two-text .room-no {
grid-column: 1;
grid-row: 1 / span 3;
margin: 0;
}
.two-text h2,
.two-text p {
grid-column: 2;
}
/* ── 반응형 ───────────────────────────────────────────────────────────── */
@ -347,6 +387,13 @@ a {
.hero-grid {
align-items: start;
}
.hero {
min-block-size: auto;
}
.hero-text {
order: -1;
padding-top: 0;
}
.hero-figure {
max-width: 26rem;
}
@ -354,6 +401,24 @@ a {
.section {
padding-block: var(--space-6);
}
.plate-head,
.two-text,
.plate-figure figcaption {
grid-template-columns: 1fr;
row-gap: var(--space-3);
}
.plate-head .room-no,
.plate-head h2,
.plate-head .lead,
.two-text .room-no,
.two-text h2,
.two-text p {
grid-column: 1;
grid-row: auto;
}
.plate-figure figcaption span {
grid-column: 1;
}
}
@media (max-width: 560px) {
.head-inner {
@ -363,6 +428,6 @@ a {
}
.nav {
width: 100%;
justify-content: space-between;
justify-content: flex-start;
}
}

View file

@ -28,10 +28,10 @@
--surface-raised: #191d1c;
--ink: #eceae2; /* 백색이 아니라 골동지의 밝기 */
--ink-muted: #a7a69b;
--ink-faint: #7b7d76;
--ink-faint: #878982;
--line: #2b302e;
--accent: #8fbcaa; /* 비색 — 텍스트용으로 밝힘. 배경 채움 금지 */
--accent-dim: #5e8274;
--accent-dim: #80ac99;
--ink-bright: #ffffff; /* CTA hover */
--space-1: 0.25rem;
@ -45,8 +45,11 @@
--radius: 2px;
--container: 1120px;
--pad-inline: clamp(1.25rem, 1rem + 2.5vw, 4rem);
/* 읽는 폭과 유물 판은 분리한다. 2560px에서도 문장이 늘어지지 않고,
빙렬 이미지만 전시 벽처럼 넓어진다. */
--container: 1200px;
--container-wide: 1600px;
--pad-inline: clamp(1.25rem, 1rem + 2.5vw, 4.5rem);
--dur-quick: 160ms;
--dur-normal: 480ms;

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,13 @@
# MaruBuri source record
Downloaded 2026-09-12 from the official NAVER Hangeul static host. The files are retained unchanged: no conversion or subsetting was performed.
| File | Official URL | HTTP content type | Size | SHA-256 |
| --- | --- | --- | --- | --- |
| `MaruBuri-Regular.woff2` | https://hangeul.pstatic.net/hangeul_static/webfont/MaruBuri/MaruBuri-Regular.woff2 | `font/woff2` | 433776 bytes | `4CF1341CF2F23FB3E263712DFDE1D8F25EEDCC328B696A2E5A2C8ADD55E5C17B` |
| `MaruBuri-SemiBold.woff2` | https://hangeul.pstatic.net/hangeul_static/webfont/MaruBuri/MaruBuri-SemiBold.woff2 | `font/woff2` | 466108 bytes | `7AEF4862FA3219B0A7DEB6746E27116A38C4069F4424EB51EDF103B29795F5C2` |
- Official stylesheet checked: https://hangeul.pstatic.net/hangeul_static/css/maru-buri.css
- Official NAVER license page saved verbatim as `LICENSE.html`: https://help.naver.com/service/11029/contents/18088?lang=ko&osType=PC
- `LICENSE.html` SHA-256: `8538F7921744D5E040F603BC301465EFBD038F1054D0754B7508DE3286452EC3`
- The saved official page includes the NAVER copyright notice and the SIL Open Font License 1.1 text for MaruBuri.

View file

@ -0,0 +1,17 @@
# 캐스크 아카이브 — 디자인 결정 기록
## 관찰과 방향
- 관찰: 어두운 앰버 표면의 병 사진은 이중 베젤과 떠 있는 태그 때문에 작은 카드처럼 보였다.
- 원리 적용 범위: 병 사진은 4:5의 큰 표면으로 두고 캐스크 번호·종류·향미 조건은 바로 이어지는 원장형 캡션으로 묶는다. 검색·필터·도슨트 세션 선택은 같은 선과 상태 대비로 읽힌다.
- 결정: 병 사진의 베젤을 제거하고 캡션을 사진 아래 정렬선에 맞췄다. 실제 수입·인증·예약을 뜻하는 문구는 예시 메타데이터와 세션 안내로 제한한다.
- 기각 조건: 긴 캐스크 메타가 320px에서 넘치거나 필터·폼 상태가 앰버 대비에서 읽히지 않으면 캡션과 컨트롤의 줄바꿈 규칙을 조정한다.
## 토큰과 검증
- 타입: 로컬 `MaruBuri` 400·600과 기존 Pretendard·모노 역할을 유지한다.
- 색: `--accent` 앰버, 어두운 `--surface`, 얇은 원장 선을 유지한다.
- 구도: 넓은 화면의 hero는 5:7 텍스트/병 사진, 병은 4:5, 캡션은 사진과 같은 폭을 사용한다.
- `verify-final` 18개 묶음은 16개가 즉시 통과했다. 전체 묶음의 invariant·visual 검사는 Gaon·Dure를 대상으로 하며, 나머지 2개는 수정 뒤 release 독립 재실행에서 ALL PASS·ALL MATCH였다.
- root는 28개 모바일, 21개 데스크톱, 7개 하위 경로와 30개 앱 상태를 직접 검토해 수용했다. stress 112건은 hard failure 0이며, 가로 넘침 4건은 targeted fix 뒤 390px 재측정에서 0이다.
- 이 기록은 렌더·계약 범위의 현재 결과다. 모든 폭·키보드 경로·인쇄 등 개별 기능을 완전 재실행했다는 뜻이 아니며, 참여자 과업 효과도 미측정이다.

View file

@ -9,10 +9,7 @@
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/webfontworld/naver/MaruBuri.css"
/>
<link rel="stylesheet" href="../styles/fonts.css?v=20260912all" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css"
@ -21,8 +18,9 @@
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap"
/>
<link rel="stylesheet" href="../styles/tokens.css" />
<link rel="stylesheet" href="../styles/main.css" />
<link rel="stylesheet" href="../styles/tokens.css?v=20260912all" />
<link rel="stylesheet" href="../styles/main.css?v=20260912all" />
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%230B0B0D'/%3E%3Ccircle cx='16' cy='16' r='10' stroke='%23D99B38' stroke-width='2' fill='none'/%3E%3Cpath d='M16 8v16M8 16h16' stroke='%23D99B38' stroke-width='1.5'/%3E%3C/svg%3E" />
<style>
.breadcrumb-bar {
padding-top: var(--space-4);
@ -82,7 +80,7 @@
.course-card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
gap: var(--space-5);
margin-top: var(--space-5);
}
@ -136,6 +134,7 @@
</style>
</head>
<body>
<a class="skip-link" href="#main-content">본문으로 건너뛰기</a>
<!-- Header (GNB SSOT) -->
<header class="site-head">
<div class="wrap head-inner">
@ -150,7 +149,7 @@
<a href="../index.html#subscribe">뉴스레터</a>
</nav>
<div class="head-actions">
<a href="#reservation-section" class="btn">도슨트 예약</a>
<a href="#reservation-section" class="btn">도슨트 세션</a>
</div>
</div>
</header>
@ -209,7 +208,7 @@
<!-- Sensory Wheel & Flavor Science -->
<section class="wrap" style="padding-bottom: var(--space-8);">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: var(--space-8); align-items: center;">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr)); gap: var(--space-8); align-items: center;">
<div class="frame">
<img
src="../assets/aroma-wheel.webp"
@ -266,9 +265,9 @@
<table class="lineup-table">
<thead>
<tr>
<th>순서</th>
<th>라인업 구성</th>
<th>ABV</th>
<th scope="col">순서</th>
<th scope="col">라인업 구성</th>
<th scope="col">ABV</th>
</tr>
</thead>
<tbody>
@ -313,9 +312,9 @@
<table class="lineup-table">
<thead>
<tr>
<th>순서</th>
<th>라인업 구성</th>
<th>ABV</th>
<th scope="col">순서</th>
<th scope="col">라인업 구성</th>
<th scope="col">ABV</th>
</tr>
</thead>
<tbody>
@ -368,7 +367,7 @@
<!-- Gastronomy Pairing Atelier -->
<section class="wrap" style="padding-bottom: var(--space-8);">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: var(--space-8); align-items: center;">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr)); gap: var(--space-8); align-items: center;">
<div style="display: flex; flex-direction: column; gap: var(--space-4);">
<div class="eyebrow">미식 페어링</div>
<h2 style="font-size: var(--step-2); font-weight: 800; line-height: 1.25;">
@ -393,14 +392,15 @@
<!-- Live Reservation Form Section -->
<section class="wrap" style="padding-bottom: var(--space-8);" id="reservation-section">
<div class="card" style="max-width: 720px; margin: 0 auto;">
<div class="eyebrow" style="color: var(--accent); margin-bottom: 4px;">예약 신청</div>
<div class="eyebrow" style="color: var(--accent); margin-bottom: 4px;">세션 항목</div>
<h2 style="font-size: var(--step-2); font-weight: 800; margin-bottom: var(--space-4);">
도슨트 테이스팅 세션 실시간 예약
도슨트 테이스팅 세션 선택
</h2>
<form onsubmit="event.preventDefault(); alert('도슨트 예약 신청이 완료되었습니다! 24시간 이내에 전담 마스터가 유선 안내를 드립니다.');" style="display: flex; flex-direction: column; gap: var(--space-4);">
<p class="demo-notice" role="note">안내: 아래 예약 항목과 가격은 예시입니다. 입력 정보는 저장·전송되지 않으며 실제 예약으로 이어지지 않습니다.</p>
<form onsubmit="event.preventDefault(); alert('선택한 도슨트 세션을 확인했습니다. 실제 예약은 접수되지 않았으며 입력 정보는 저장하거나 전송하지 않습니다.');" style="display: flex; flex-direction: column; gap: var(--space-4);">
<div class="form-group">
<label class="form-label" for="courseSelect">선택 코스</label>
<select class="form-input" id="courseSelect" required>
<select class="form-input" id="courseSelect" name="courseSelect" required>
<option value="courseB">코스 B. 캐스크 스트렝스 & 빈티지 마스터 플라이트 (120,000원)</option>
<option value="courseA">코스 A. 독립병입자 입문 탐색 플라이트 (65,000원)</option>
</select>
@ -409,11 +409,11 @@
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3);">
<div class="form-group">
<label class="form-label" for="resDate">예약 일자</label>
<input type="date" class="form-input" id="resDate" value="2026-08-28" required />
<input type="date" class="form-input" id="resDate" name="reservationDate" autocomplete="off" required />
</div>
<div class="form-group">
<label class="form-label" for="resTime">예약 시간대</label>
<select class="form-input" id="resTime" required>
<select class="form-input" id="resTime" name="reservationTime" required>
<option>19:00 (1부)</option>
<option selected>20:30 (2부)</option>
<option>22:00 (3부 - 심야 세션)</option>
@ -424,21 +424,21 @@
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3);">
<div class="form-group">
<label class="form-label" for="userName">예약자 성함</label>
<input type="text" class="form-input" id="userName" value="정재훈" required />
<input type="text" class="form-input" id="userName" name="userName" autocomplete="name" required />
</div>
<div class="form-group">
<label class="form-label" for="userPhone">연락처</label>
<input type="tel" class="form-input" id="userPhone" value="010-8831-2094" required />
<input type="tel" class="form-input" id="userPhone" name="userPhone" autocomplete="tel" required />
</div>
</div>
<div class="form-group">
<label class="form-label" for="flavorNote">평소 선호하시는 위스키 취향 (선택)</label>
<input type="text" class="form-input" id="flavorNote" placeholder="예: 아일라 피트 위스키, 셰리 버트 숙성 선호" value="셰리 버트 숙성 & 80ppm 이상 헤비 피트 선호" />
<input type="text" class="form-input" id="flavorNote" name="flavorNote" autocomplete="off" placeholder="예: 아일라 피트 위스키, 셰리 버트 숙성 선호" />
</div>
<button type="submit" class="btn-primary" style="margin-top: var(--space-2); padding: 14px; font-size: var(--step-0);">
위스키 도슨트 세션 예약 확정 →
선택한 세션 내용 확인 →
</button>
</form>
</div>
@ -448,9 +448,8 @@
<!-- 푸터 (.site-foot SSOT) -->
<footer class="site-foot">
<div class="wrap foot-inner">
<p>캐스크 아카이브 · 서울 용산구 한남대로 27길 19 지하 1층</p>
<p class="muted">매일 18:00 – 02:00 (사전 예약제)</p>
<p class="muted">bottling@caskarchive.kr</p>
<p>캐스크 아카이브 · 독립병입 위스키 안내</p>
<p class="muted">이 페이지의 코스·가격·신청 흐름은 예시이며 입력 정보는 저장·전송되지 않습니다.</p>
</div>
</footer>
</body>

View file

@ -3,23 +3,20 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>캐스크 아카이브 (CASK ARCHIVE) · 독립병입자 위스키바 & 도슨트 큐레이션</title>
<title>캐스크 아카이브 (CASK ARCHIVE) · 독립병입 위스키 안내</title>
<meta
name="description"
content="Gordon & MacPhail, Signatory, SMWS 등 독립병입자(IB) 희귀 캐스크 공식 수입 인증 위스키바. 캐스크 넘버 원장과 1:1 위스키 도슨트 테이스팅 코스 예약."
content="독립병입 위스키의 숙성 정보와 향미를 비교해 보는 안내입니다. 원장·가격·세션 정보는 예시이며 실제 예약을 받지 않습니다."
/>
<meta property="og:title" content="캐스크 아카이브 (CASK ARCHIVE) · 독립병입자 위스키바" />
<meta property="og:description" content="시간과 배럴이 빚어낸 오직 단 하나의 원액. 공식 수입 인증 독립병입자 전문 아카이브." />
<meta property="og:title" content="캐스크 아카이브 (CASK ARCHIVE) · 독립병입 위스키 안내" />
<meta property="og:description" content="독립병입 위스키의 숙성 정보와 향미를 비교해 보는 안내입니다." />
<meta property="og:type" content="website" />
<meta name="theme-color" content="#0b0b0d" />
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/webfontworld/naver/MaruBuri.css"
/>
<link rel="stylesheet" href="./styles/fonts.css?v=20260912all" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css"
@ -28,7 +25,7 @@
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap"
/>
<link rel="stylesheet" href="./styles/main.css?v=4" />
<link rel="stylesheet" href="./styles/main.css?v=20260912all" />
<link
rel="icon"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%230B0B0D'/%3E%3Ccircle cx='16' cy='16' r='10' stroke='%23D99B38' stroke-width='2' fill='none'/%3E%3Cpath d='M16 8v16M8 16h16' stroke='%23D99B38' stroke-width='1.5'/%3E%3C/svg%3E"
@ -55,7 +52,7 @@
<div class="head-actions">
<a href="#docent" class="btn">
도슨트 예약
도슨트 세션
</a>
</div>
</div>
@ -67,29 +64,29 @@
<div class="wrap hero-grid">
<div class="hero-content">
<div class="eyebrow-amber">
<span>OFFICIALLY CERTIFIED INDEPENDENT BOTTLER BAR</span>
<span>INDEPENDENT BOTTLER TASTING ARCHIVE</span>
</div>
<h1 id="heroHeading" class="hero-title">
시간과 배럴이 빚어낸<br />
오직 단 하나의 <span class="gold">원액(Single Cask)</span>
시간과 배럴이 만든<br />
단 하나의 <span class="gold">캐스크 원액</span>
</h1>
<p class="hero-desc">
증류소의 기본 공식 라인업에서는 맛볼 수 없는 독립병입자(Independent Bottler)의 희귀 원액들. 공식 정식 수입 필증과 단일 캐스크 일련번호(Single Cask Number)를 보증하며, 전담 도슨트가 취향에 맞춘 위스키를 1:1로 큐레이션합니다.
독립병입 위스키의 숙성 정보와 향미를 비교하며, 취향에 맞는 테이스팅 선택 방식을 살펴볼 수 있습니다.
</p>
<div class="hero-ctas">
<a href="#ledger" class="btn-amber">
실시간 캐스크 원장 확인
캐스크 목록 살펴보기
<span aria-hidden="true">↗</span>
</a>
<a href="./docent/index.html" class="btn-outline">
1:1 도슨트 마스터클래스 상세 보기
도슨트 세션 상세 보기
</a>
</div>
<p class="demo-notice" role="note">안내: 이 페이지의 원장·인증 정보·가격·예약·구독은 예시입니다. 입력 정보는 저장·전송되지 않으며 실제 예약 또는 구독으로 이어지지 않습니다.</p>
</div>
<div class="hero-visual" style="position: relative;">
<div class="bezel-outer">
<div class="hero-frame">
<div class="hero-visual">
<figure class="hero-frame">
<img
src="./assets/whisky-hero.webp"
alt="차링된 오크 배럴 상판 위 싱글 캐스크 위스키 보틀과 테이스팅 글라스"
@ -97,44 +94,44 @@
height="1600"
loading="eager"
/>
</div>
</div>
<div class="hero-cask-tag">
<figcaption class="hero-cask-tag">
<div class="cask-tag-no">CASK #8821 · 58.4% ABV</div>
<div class="cask-tag-sub">Signatory Vintage 1997 · Bottle 42 of 186</div>
</div>
</figcaption>
</figure>
</div>
</div>
</section>
<!-- 공식 독립병입자 수입 인증 스트립 -->
<section class="ib-cert-strip" aria-label="독립병입자 공식 인증">
<section class="ib-cert-strip" aria-label="독립병입 위스키 정보 예시">
<div class="wrap cert-grid">
<div class="cert-item">
<div class="cert-name">GORDON & MACPHAIL</div>
<div class="cert-desc">1895년 설립 · 스코틀랜드 최고 권위 정식 수입</div>
<div class="cert-desc">병입자 표기를 원장에 연결하는 예시</div>
</div>
<div class="cert-item">
<div class="cert-name">SIGNATORY VINTAGE</div>
<div class="cert-desc">언칠필터드 / 내추럴 컬러 캐스크 스트렝스 보증</div>
<div class="cert-desc">숙성 조건과 도수 메타데이터 예시</div>
</div>
<div class="cert-item">
<div class="cert-name">THE SCOTCH MALT WHISKY SOCIETY</div>
<div class="cert-desc">SMWS 공식 파트너 베뉴 · 넘버링 큐레이션</div>
<div class="cert-desc">캐스크 번호와 향미 기준의 연결 예시</div>
</div>
<div class="cert-item">
<div class="cert-name">CADENHEAD'S</div>
<div class="cert-desc">1842년 설립, 스코틀랜드에서 가장 오래된 독립병입자 라인업</div>
</div>
</div>
<div class="wrap"><p class="demo-notice" role="note">아래 병입자·수입 정보는 예시입니다. 실제 인증이나 입고 현황을 뜻하지 않습니다.</p></div>
</section>
<!-- 캐스크 원장 (Interactive Cask Ledger) -->
<section class="ledger-section" id="ledger" aria-labelledby="ledgerHeading">
<div class="wrap">
<div class="section-head">
<h2 id="ledgerHeading" class="section-title">독립병입자 캐스크 실시간 보유 원장</h2>
<p class="section-sub">모든 병은 단일 캐스크(Single Cask)에서 추출되어 병입 수량이 한정되어 있습니다. 필터로 원하는 향미 스펙을 검색하십시오.</p>
<h2 id="ledgerHeading" class="section-title">독립병입 캐스크 목록</h2>
<p class="section-sub">아래 병입 정보·수량·인증 표시는 예시입니다. 향미 기준으로 원하는 캐스크를 비교해 보세요.</p>
</div>
<div class="ledger-filter-bar">
@ -149,7 +146,7 @@
type="search"
id="caskSearch"
class="search-input"
placeholder="증류소명 or 캐스크 번호 검색..."
placeholder="증류소명 또는 캐스크 번호 검색"
aria-label="캐스크 검색"
/>
</div>
@ -158,14 +155,14 @@
<table class="ledger-table">
<thead>
<tr>
<th>캐스크 번호</th>
<th>증류소 & 원액 명칭</th>
<th>독립병입자 (Bottler)</th>
<th>숙성 배럴 / 년수</th>
<th>도수 (ABV)</th>
<th>한정 수량</th>
<th>향미 노트 요약</th>
<th>인증 정보</th>
<th scope="col">캐스크 번호</th>
<th scope="col">증류소 & 원액 명칭</th>
<th scope="col">독립병입자 (Bottler)</th>
<th scope="col">숙성 배럴 / 년수</th>
<th scope="col">도수 (ABV)</th>
<th scope="col">한정 수량</th>
<th scope="col">향미 노트 요약</th>
<th scope="col">인증 정보</th>
</tr>
</thead>
<tbody id="caskTableBody">
@ -177,7 +174,7 @@
<td class="mono" style="color:var(--accent);font-weight:700;">54.2%</td>
<td class="mono">180 Btls</td>
<td>말린 무화과, 밀랍, 시나몬, 다크초콜릿</td>
<td><button class="btn-outline" style="padding:4px 10px;font-size:11px;" onclick="openCertModal('Clynelish 1996 #6512', 'Signatory Vintage', '24 Years (1996 Distilled / 2020 Bottled)', '54.2% Cask Strength', '식약처 정식통관 제2024-IB-0982호');">수입인증서</button></td>
<td><button class="btn-outline" style="padding:4px 10px;font-size:11px;" onclick="openCertModal('Clynelish 1996 #6512', 'Signatory Vintage', '24 Years (1996 Distilled / 2020 Bottled)', '54.2% Cask Strength', '식약처 정식통관 제2024-IB-0982호');">병입 정보 보기</button></td>
</tr>
<tr data-cat="peat cs">
<td class="mono cask-badge">#29.281</td>
@ -187,7 +184,7 @@
<td class="mono" style="color:var(--accent);font-weight:700;">57.8%</td>
<td class="mono">214 Btls</td>
<td>모닥불 연기, 바다 소금, 레몬 제스트, 피트 45ppm</td>
<td><button class="btn-outline" style="padding:4px 10px;font-size:11px;" onclick="openCertModal('Laphroaig 2005 (SMWS 29.281)', 'The SMWS Partner', '15 Years (2005 Distilled)', '57.8% Cask Strength', '식약처 정식통관 제2025-IB-1102호');">수입인증서</button></td>
<td><button class="btn-outline" style="padding:4px 10px;font-size:11px;" onclick="openCertModal('Laphroaig 2005 (SMWS 29.281)', 'The SMWS Partner', '15 Years (2005 Distilled)', '57.8% Cask Strength', '식약처 정식통관 제2025-IB-1102호');">병입 정보 보기</button></td>
</tr>
<tr data-cat="sherry">
<td class="mono cask-badge">#93/41</td>
@ -197,7 +194,7 @@
<td class="mono" style="color:var(--accent);font-weight:700;">51.6%</td>
<td class="mono">142 Btls</td>
<td>열대 과일 잼, 가죽, 부드러운 유황, 정향</td>
<td><button class="btn-outline" style="padding:4px 10px;font-size:11px;" onclick="openCertModal('Springbank 1993 #93/41', 'Gordon & MacPhail Reserve', '27 Years (1993 Distilled)', '51.6%', '식약처 정식통관 제2024-IB-0419호');">수입인증서</button></td>
<td><button class="btn-outline" style="padding:4px 10px;font-size:11px;" onclick="openCertModal('Springbank 1993 #93/41', 'Gordon & MacPhail Reserve', '27 Years (1993 Distilled)', '51.6%', '식약처 정식통관 제2024-IB-0419호');">병입 정보 보기</button></td>
</tr>
<tr data-cat="peat cs">
<td class="mono cask-badge">#PX-88</td>
@ -207,7 +204,7 @@
<td class="mono" style="color:var(--accent);font-weight:700;">56.4%</td>
<td class="mono">260 Btls</td>
<td>스모크 베이컨, 건포도, 블랙베리, 훈제 파프리카</td>
<td><button class="btn-outline" style="padding:4px 10px;font-size:11px;" onclick="openCertModal('Caol Ila 2008 #PX-88', 'Cadenheads Authentic Collection', '14 Years', '56.4% Cask Strength', '식약처 정식통관 제2025-IB-0831호');">수입인증서</button></td>
<td><button class="btn-outline" style="padding:4px 10px;font-size:11px;" onclick="openCertModal('Caol Ila 2008 #PX-88', 'Cadenheads Authentic Collection', '14 Years', '56.4% Cask Strength', '식약처 정식통관 제2025-IB-0831호');">병입 정보 보기</button></td>
</tr>
<tr data-cat="bourbon cs">
<td class="mono cask-badge">#98/12</td>
@ -217,7 +214,7 @@
<td class="mono" style="color:var(--accent);font-weight:700;">58.1%</td>
<td class="mono">195 Btls</td>
<td>바닐라 커스터드, 구운 코코넛, 청사과, 후추</td>
<td><button class="btn-outline" style="padding:4px 10px;font-size:11px;" onclick="openCertModal('Ben Nevis 1998 #98/12', 'Adelphi Selection', '22 Years', '58.1%', '식약처 정식통관 제2024-IB-0771호');">수입인증서</button></td>
<td><button class="btn-outline" style="padding:4px 10px;font-size:11px;" onclick="openCertModal('Ben Nevis 1998 #98/12', 'Adelphi Selection', '22 Years', '58.1%', '식약처 정식통관 제2024-IB-0771호');">병입 정보 보기</button></td>
</tr>
</tbody>
</table>
@ -262,12 +259,12 @@
<!-- 예약 입력 폼 -->
<div class="docent-booking-form">
<h3 style="font-family: var(--font-serif); font-size: var(--step-1); margin-bottom: var(--space-4); color: var(--ink);">
도슨트 프라이빗 세션 예약 신청
도슨트 프라이빗 세션 항목
</h3>
<form id="docentForm" onsubmit="return false;">
<div class="form-group">
<label class="form-label" for="bookingCourse">선택 코스</label>
<select id="bookingCourse" class="form-select">
<select id="bookingCourse" name="bookingCourse" class="form-select">
<option value="A">코스 A. 독립병입자 입문 탐색 플라이트 (65,000원)</option>
<option value="B" selected>코스 B. 캐스크 스트렝스 & 빈티지 마스터 플라이트 (120,000원)</option>
</select>
@ -276,11 +273,11 @@
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem;">
<div class="form-group">
<label class="form-label" for="bookingDate">예약 일자</label>
<input type="date" id="bookingDate" class="form-input" value="2026-08-28" />
<input type="date" id="bookingDate" name="bookingDate" autocomplete="off" class="form-input" />
</div>
<div class="form-group">
<label class="form-label" for="bookingTime">예약 시간대</label>
<select id="bookingTime" class="form-select">
<select id="bookingTime" name="bookingTime" class="form-select">
<option>19:00 (1부)</option>
<option selected>20:30 (2부)</option>
<option>22:00 (3부 심야)</option>
@ -291,21 +288,21 @@
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem;">
<div class="form-group">
<label class="form-label" for="guestName">예약자 성함</label>
<input type="text" id="guestName" class="form-input" placeholder="홍길동" value="정재훈" required />
<input type="text" id="guestName" name="guestName" autocomplete="name" class="form-input" placeholder="성함" required />
</div>
<div class="form-group">
<label class="form-label" for="guestPhone">연락처</label>
<input type="tel" id="guestPhone" class="form-input" placeholder="010-0000-0000" value="010-8831-2094" required />
<input type="tel" id="guestPhone" name="guestPhone" autocomplete="tel" class="form-input" placeholder="연락처" required />
</div>
</div>
<div class="form-group">
<label class="form-label" for="guestPref">향미 선호도 메모</label>
<input type="text" id="guestPref" class="form-input" placeholder="예: 셰리 밤 선호, 스모키 피트 선호 등" value="강렬한 셰리 버트와 헤비 피트 선호" />
<input type="text" id="guestPref" name="guestPreference" autocomplete="off" class="form-input" placeholder="예: 셰리 밤 선호, 스모키 피트 선호 등" />
</div>
<button type="button" class="btn-amber" id="btnBookDocent" style="width: 100%; padding: 14px; margin-top: 10px;">
위스키 도슨트 세션 예약 확정
선택한 세션 내용 확인
</button>
</form>
</div>
@ -360,25 +357,28 @@
신규 캐스크 입고 알림 & 인스타그램
</h3>
<p style="font-size: var(--step--1); color: var(--ink-muted); max-width: 500px; margin-inline: auto;">
수량이 극소량인 독립병입자 캐스크 오프닝 소식을 이메일 뉴스레터로 가장 먼저 발송해 드립니다.
신규 캐스크와 테이스팅 콘텐츠를 둘러보세요.
</p>
<p class="demo-notice" role="note">이메일은 이 화면에 저장되지 않으며 실제 구독으로 등록되지 않습니다.</p>
<form class="newsletter-form" onsubmit="return false;">
<input
type="email"
id="newsEmail"
name="newsletterEmail"
autocomplete="email"
class="form-input"
placeholder="귀하의 이메일 주소를 입력하세요"
required
/>
<button type="button" class="btn-amber" id="btnSubscribe">
구독 신청
입력 내용 확인
</button>
</form>
<div style="margin-top: var(--space-5); display: inline-flex; align-items: center; gap: 8px;">
<a href="https://instagram.com" target="_blank" rel="noopener noreferrer" class="btn-outline" style="font-size: 12px; padding: 6px 16px;">
📸 인스타그램 @caskarchive.official 바로가기 ↗
📸 인스타그램 살펴보기 ↗
</a>
</div>
</div>
@ -390,7 +390,7 @@
<dialog id="certModal" class="cert-dialog" aria-labelledby="certTitle">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; border-bottom: 1px solid var(--line); padding-bottom: 0.75rem;">
<h3 id="certTitle" style="font-family: var(--font-serif); font-size: var(--step-1); color: var(--accent);">
공식 독립병입자 수입 보증서
병입 정보 예시
</h3>
<button type="button" onclick="document.getElementById('certModal').close();" aria-label="닫기" style="font-size: 18px; font-weight: 700; color: var(--ink-muted);">✕</button>
</div>
@ -409,9 +409,8 @@
<!-- 푸터 (.site-foot SSOT) -->
<footer class="site-foot">
<div class="wrap foot-inner">
<p>캐스크 아카이브 · 서울 용산구 한남대로 27길 19 지하 1층</p>
<p class="muted">매일 18:00 – 02:00 (사전 예약제)</p>
<p class="muted">bottling@caskarchive.kr</p>
<p>캐스크 아카이브 · 독립병입 위스키 안내</p>
<p class="muted">이 페이지의 원장·가격·신청 흐름은 예시이며 입력 정보는 저장·전송되지 않습니다.</p>
</div>
</footer>
@ -458,7 +457,7 @@
const date = document.getElementById("bookingDate").value;
const time = document.getElementById("bookingTime").value;
const course = document.getElementById("bookingCourse").value;
alert(`[캐스크 아카이브] ${name}님의 1:1 위스키 도슨트 세션(코스 ${course})이 예약 접수되었습니다.\n일정: ${date} ${time}\n담당 도슨트가 당일 사전 안내 연락을 드립니다.`);
alert(`${name}님이 선택한 도슨트 세션(코스 ${course})을 확인했습니다.\n실제 예약은 접수되지 않았으며 입력 정보는 저장하거나 전송하지 않습니다.`);
});
}
@ -471,7 +470,7 @@
alert("유효한 이메일 주소를 입력해 주십시오.");
return;
}
alert(`[캐스크 아카이브] ${email} 주소로 신규 독립병입자 캐스크 릴리즈 알림이 등록되었습니다.`);
alert(`${email} 주소는 이 화면에 저장되지 않으며 실제 구독으로 등록되지 않습니다.`);
document.getElementById("newsEmail").value = "";
});
}

View file

@ -0,0 +1,15 @@
@font-face {
font-family: "MaruBuri";
src: url("../assets/fonts/MaruBuri-Regular.woff2") format("woff2");
font-style: normal;
font-weight: 400;
font-display: swap;
}
@font-face {
font-family: "MaruBuri";
src: url("../assets/fonts/MaruBuri-SemiBold.woff2") format("woff2");
font-style: normal;
font-weight: 600;
font-display: swap;
}

View file

@ -1,4 +1,4 @@
@import "./tokens.css";
@import "./tokens.css?v=20260912all";
/* ==========================================================================
캐스크 아카이브 (CASK ARCHIVE) — 메인 스타일시트 SSOT
@ -266,7 +266,7 @@ button {
.hero-grid {
display: grid;
grid-template-columns: minmax(0, 1.25fr) minmax(0, 0.95fr);
grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
gap: var(--space-7);
align-items: center;
}
@ -304,15 +304,8 @@ button {
position: relative;
}
.bezel-outer {
background: rgba(217, 155, 56, 0.06);
border: 1px solid var(--line);
padding: 6px;
border-radius: var(--radius-md);
}
.hero-frame {
border-radius: calc(var(--radius-md) - 4px);
margin: 0;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 20px 48px -12px rgba(0, 0, 0, 0.7);
@ -321,17 +314,15 @@ button {
.hero-frame img {
width: 100%;
aspect-ratio: 4/3;
aspect-ratio: 4/5;
object-fit: cover;
display: block;
}
.hero-cask-tag {
margin-top: var(--space-2);
padding: 6px 12px;
background: var(--surface-raised);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: var(--space-3) 0 0;
background: transparent;
border: 0;
display: flex;
justify-content: space-between;
align-items: baseline;
@ -390,9 +381,9 @@ button {
}
.section-head {
text-align: center;
max-width: 680px;
margin-inline: auto;
text-align: left;
max-width: 48rem;
margin-inline: 0;
margin-bottom: var(--space-6);
}
@ -439,7 +430,7 @@ button {
color: var(--ink-muted);
font-size: 12px;
font-weight: 600;
transition: all var(--dur-quick);
transition: background-color var(--dur-quick), border-color var(--dur-quick), color var(--dur-quick);
}
.filter-btn:hover {
@ -726,11 +717,14 @@ button {
inset: 0;
margin: auto;
width: min(90vw, 480px);
max-height: calc(100dvh - (var(--space-4) * 2));
background: #141418;
color: #f3f3f5;
border: 1px solid rgba(217, 155, 56, 0.4);
border-radius: var(--radius-sm);
padding: var(--space-5);
overflow: auto;
overscroll-behavior: contain;
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.8);
outline: none;
}
@ -784,3 +778,60 @@ button {
display: none;
}
}
html:has(dialog[open]) {
overflow: hidden;
}
/* 예약·원장 표면의 조작 여백과 키보드 상태를 토큰으로 맞춘다. */
:where(a, button, input, select, textarea):focus-visible {
outline: 3px solid var(--accent);
outline-offset: 2px;
}
.btn-amber,
.btn-outline,
.head-actions .btn,
.filter-btn,
.search-input,
.form-input,
.form-select,
.newsletter-form input {
min-height: var(--control-block);
padding-inline: var(--control-pad-inline);
}
.ledger-table th,
.ledger-table td {
padding-inline: var(--control-pad-inline);
}
.demo-notice {
max-width: 58ch;
margin-top: var(--space-4);
padding: var(--space-3) var(--space-4);
border-left: 3px solid var(--accent);
background: var(--accent-light);
color: var(--ink-muted);
font-size: var(--step--1);
line-height: 1.55;
word-break: keep-all;
}
@media (max-width: 600px) {
.head-actions .btn { display: none; }
.hero-title { font-size: clamp(1.65rem, 7.8vw, 2.15rem); }
.hero-ctas { align-items: stretch; }
.hero-ctas > a { width: 100%; }
.course-header { align-items: flex-start; flex-direction: column; gap: var(--space-2); }
.docent-booking-form { padding: var(--space-5); }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 1ms !important;
animation-iteration-count: 1 !important;
scroll-behavior: auto !important;
transition-duration: 1ms !important;
}
}

View file

@ -64,8 +64,10 @@
--space-8: clamp(4.5rem, 3rem + 6vw, 9rem);
/* 컨테이너 및 패딩 — 전체 쇼케이스 절대 통일 SSOT */
--container: 1120px;
--container: clamp(70rem, 62vw, 85rem);
--pad-inline: clamp(1.25rem, 1rem + 2.5vw, 4rem);
--control-block: 2.75rem;
--control-pad-inline: 0.875rem;
/* 모션 */
--dur-quick: 180ms;

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,13 @@
# MaruBuri source record
Downloaded 2026-09-12 from the official NAVER Hangeul static host. The files are retained unchanged: no conversion or subsetting was performed.
| File | Official URL | HTTP content type | Size | SHA-256 |
| --- | --- | --- | --- | --- |
| `MaruBuri-Regular.woff2` | https://hangeul.pstatic.net/hangeul_static/webfont/MaruBuri/MaruBuri-Regular.woff2 | `font/woff2` | 433776 bytes | `4CF1341CF2F23FB3E263712DFDE1D8F25EEDCC328B696A2E5A2C8ADD55E5C17B` |
| `MaruBuri-SemiBold.woff2` | https://hangeul.pstatic.net/hangeul_static/webfont/MaruBuri/MaruBuri-SemiBold.woff2 | `font/woff2` | 466108 bytes | `7AEF4862FA3219B0A7DEB6746E27116A38C4069F4424EB51EDF103B29795F5C2` |
- Official stylesheet checked: https://hangeul.pstatic.net/hangeul_static/css/maru-buri.css
- Official NAVER license page saved verbatim as `LICENSE.html`: https://help.naver.com/service/11029/contents/18088?lang=ko&osType=PC
- `LICENSE.html` SHA-256: `8538F7921744D5E040F603BC301465EFBD038F1054D0754B7508DE3286452EC3`
- The saved official page includes the NAVER copyright notice and the SIL Open Font License 1.1 text for MaruBuri.

View file

@ -0,0 +1,17 @@
# 청온 케어 — 디자인 결정 기록
## 관찰과 방향
- 관찰: 기존 첫 화면은 카드 안의 사진과 떠 있는 수치 배지가 서비스 범위보다 먼저 읽혔다.
- 원리 적용 범위: 소개와 견적 행동은 왼쪽 읽기 폭에, 생활 공간 사진과 32개 검수 항목 예시는 오른쪽 한 장면에 둔다. 견적은 입력 작업표와 합계 명세를 분리한다.
- 결정: 이중 프레임·떠 있는 배지를 제거하고 사진 캡션으로 검수 항목의 맥락을 연결했다. 성능 수치·인증·의학적 표현은 근거가 없으므로 작업표에 기록할 확인 절차로 바꿨다.
- 기각 조건: 사진이 견적 행동을 밀거나 작은 폭에서 입력과 합계가 동시에 읽히지 않으면 1열 순서와 간격을 다시 조정한다.
## 토큰과 검증
- 타입: `MaruBuri` 로컬 400·600은 제목 역할, Pretendard는 본문·입력 역할이다. 본문은 `--step-0`, 보조 기록은 `--step--1`을 유지한다.
- 색: 기존 청록 `--accent`, 밝은 표면과 선 토큰을 유지한다. 청록은 상태·행동과 검수 표시에만 쓴다.
- 구도: 넓은 화면은 5:7 소개/사진, 견적은 입력/합계 두 열이며 860px 이하에서는 한 열이다.
- `verify-final` 18개 묶음은 16개가 즉시 통과했다. 전체 묶음의 invariant·visual 검사는 Gaon·Dure를 대상으로 하며, 나머지 2개는 수정 뒤 release 독립 재실행에서 ALL PASS·ALL MATCH였다.
- root는 28개 모바일, 21개 데스크톱, 7개 하위 경로와 30개 앱 상태를 직접 검토해 수용했다. stress 112건은 hard failure 0이며, 가로 넘침 4건은 targeted fix 뒤 390px 재측정에서 0이다.
- 이 기록은 렌더·계약 범위의 현재 결과다. 모든 폭·키보드 경로·인쇄 등 개별 기능을 완전 재실행했다는 뜻이 아니며, 참여자 과업 효과도 미측정이다.

View file

@ -3,10 +3,10 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>청온 케어 · 프리미엄 주거·오피스 전문 위생 케어 & 관제 콘솔</title>
<title>청온 케어 · 프리미엄 주거·오피스 위생 케어 안내</title>
<meta
name="description"
content="임상 수준의 6단계 위생 프로토콜과 100% 정찰제 견적. 독일 카처 4.0bar 고온스팀과 에코서트 인증 천연 세제. 실시간 견적 신청 및 관제 콘솔 지원."
content="위생 케어 절차, 예상 견적, 운영 정보의 표시 방식을 소개합니다. 가격과 접수 정보는 예시이며 실제 신청은 진행되지 않습니다."
/>
<meta property="og:title" content="청온 케어 · 프리미엄 위생 케어" />
<meta property="og:description" content="공간의 숨결을 되돌리는 6단계 프로토콜과 투명한 정찰제 견적." />
@ -24,7 +24,8 @@
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap"
/>
<link rel="stylesheet" href="./styles/main.css?v=1" />
<link rel="stylesheet" href="./styles/fonts.css?v=20260912all" />
<link rel="stylesheet" href="./styles/main.css?v=20260912all" />
<link
rel="icon"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%230D9488'/%3E%3Cpath d='M9 16.5l5 5 9-10' stroke='%23ffffff' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/%3E%3C/svg%3E"
@ -56,7 +57,7 @@
</button>
<button type="button" class="mode-btn" id="btnModeAdmin" title="단축키: Alt + A">
<span class="badge-dot" aria-hidden="true"></span>
관리자 콘솔
운영 현황
</button>
</div>
<a href="#calculator" class="btn client-only-btn">
@ -76,43 +77,38 @@
<div class="wrap hero-grid">
<div class="hero-content">
<div class="eyebrow">
<span>임상 수준 위생 프로토콜</span>
<span>현장 검수 절차 예시</span>
</div>
<h1 id="heroHeading" class="hero-title">
보이지 않는 미세 오염까지,<br />
<em>공간의 숨결</em>을 되돌립니다
보이지 않는 오염까지,<br />
<em>공간을 새롭게</em> 돌봅니다
</h1>
<p class="hero-desc">
단순한 미화가 아닙니다. 독일 카처 4.0bar 고온 스팀과 에코서트(ECOCERT) 인증 친환경 세제로 새집증후군, 미세 분진, 알레르기 유발 물질을 근본적으로 제거합니다.
공간 유형과 선택 항목을 바탕으로 케어 범위와 예상 금액을 살펴볼 수 있습니다.
</p>
<div class="hero-ctas">
<a href="#calculator" class="btn-primary">
실시간 정찰제 견적 계산하기
항목별 예상 견적 보기
<span class="btn-icon-circle" aria-hidden="true">↗</span>
</a>
<div class="hero-trust">
<span class="hero-trust-stars">★★★★★</span>
<span>누적 케어 4,820세대 · 만족도 99.4%</span>
<span>선택 항목별 예상 금액을 확인하세요</span>
</div>
</div>
<p class="demo-notice" role="note">안내: 이 페이지의 서비스·가격·운영 현황은 예시입니다. 입력 정보는 저장·전송되지 않으며 실제 견적이나 신청으로 이어지지 않습니다.</p>
</div>
<div class="hero-visual">
<div class="bezel-outer">
<div class="hero-frame">
<img
src="./assets/clean-hero.webp"
alt="정밀 입주 케어가 완료된 미니멀한 프리미엄 주거 거실 전경"
width="1200"
height="900"
loading="eager"
/>
</div>
</div>
<div class="hero-stat-floating">
<span class="stat-val">32 Point</span>
<span class="stat-label">마스터 클리너<br />정밀 검수 체크리스트</span>
</div>
<figure class="hero-frame">
<img
src="./assets/clean-hero.webp"
alt="정돈된 주거 공간의 거실 전경"
width="1200"
height="900"
loading="eager"
/>
<figcaption>주거 공간 케어 예시 · 현장에서는 32개 검수 항목을 확인하는 흐름을 표시합니다.</figcaption>
</figure>
</div>
</div>
</section>
@ -121,8 +117,9 @@
<section class="calc-section" id="calculator" aria-labelledby="calcHeading">
<div class="wrap">
<div class="section-head">
<h2 id="calcHeading" class="section-title">투명한 정찰제 실시간 견적 산정</h2>
<p class="section-sub">추가금 없는 명확한 기준. 공간 면적과 맞춤 옵션을 선택하시면 즉시 예상 금액이 산출됩니다.</p>
<p class="eyebrow">견적 작업표</p>
<h2 id="calcHeading" class="section-title">항목별 예상 견적</h2>
<p class="section-sub">공간 면적과 선택 항목을 기준으로 예상 금액의 구성 방식을 확인합니다.</p>
</div>
<div class="calc-grid">
@ -153,6 +150,7 @@
<input
type="range"
id="pyeongSlider"
name="spaceSize"
class="range-input"
min="12"
max="75"
@ -167,11 +165,11 @@
<div class="addon-grid">
<label class="addon-label">
<input type="checkbox" name="addon" value="steam" data-price="45000" checked />
<span>160℃ 고온 살균 스팀 (+45,000원)</span>
<span>고온 스팀 작업 기록 (+45,000원)</span>
</label>
<label class="addon-label">
<input type="checkbox" name="addon" value="phytoncide" data-price="35000" checked />
<span>편백 피톤치드 훈증 (+35,000원)</span>
<span>향·마감 작업 기록 (+35,000원)</span>
</label>
<label class="addon-label">
<input type="checkbox" name="addon" value="aircon" data-price="80000" />
@ -187,30 +185,36 @@
<!-- 예약 정보 입력 -->
<div class="form-group" style="margin-top: 1.5rem;">
<span class="form-label">신청자 정보</span>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 0.75rem;">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 0.75rem;">
<label class="sr-only" for="clientName">신청자 성함</label>
<input
type="text"
id="clientName"
name="clientName"
autocomplete="name"
placeholder="성함 / 고객명"
style="width: 100%; min-width: 0; padding: 10px; border: 1px solid var(--line); border-radius: var(--radius-sm); font-size: var(--step--1);"
value="이서연"
required
/>
<label class="sr-only" for="clientPhone">신청자 연락처</label>
<input
type="tel"
id="clientPhone"
name="clientPhone"
autocomplete="tel"
placeholder="연락처 (010-0000-0000)"
style="width: 100%; min-width: 0; padding: 10px; border: 1px solid var(--line); border-radius: var(--radius-sm); font-size: var(--step--1);"
value="010-4821-9930"
required
/>
</div>
<label class="sr-only" for="clientAddress">케어 현장 주소</label>
<input
type="text"
id="clientAddress"
name="clientAddress"
autocomplete="off"
placeholder="케어 현장 주소 (예: 서울시 마포구 공덕동 120)"
style="width: 100%; min-width: 0; margin-top: 0.5rem; padding: 10px; border: 1px solid var(--line); border-radius: var(--radius-sm); font-size: var(--step--1);"
value="서울시 성동구 성수동2가 아크로포레스트 102동"
required
/>
</div>
@ -219,7 +223,7 @@
<!-- 요약 및 접수 CTA -->
<div class="calc-summary">
<h3 class="summary-title">실시간 견적 명세서</h3>
<h3 class="summary-title">예상 견적 명세</h3>
<div class="summary-row">
<span>선택 공간</span>
<span id="sumSpaceType" class="mono">주거 입주/이사</span>
@ -242,10 +246,10 @@
</div>
<button type="button" class="calc-submit" id="btnSubmitQuote">
이 견적으로 신청 접수하기
예상 견적 보기
</button>
<p style="font-size: 12px; color: var(--ink-inverse-muted); margin-top: 10px; text-align: center;">
✓ 접수 즉시 배차 팀장이 연락 후 사전 현장 조율을 진행합니다.
<p style="font-size: 12px; color: var(--ink-muted); margin-top: 10px; text-align: center;">
선택 항목에 따른 예상 금액을 확인할 수 있습니다. 실제 견적·신청은 진행되지 않습니다.
</p>
</div>
</div>
@ -257,11 +261,11 @@
<div class="wrap">
<div class="section-head" style="display: flex; justify-content: space-between; align-items: flex-end; flex-wrap: wrap; gap: 12px;">
<div>
<h2 id="protoHeading" class="section-title">임상 수준의 6단계 청온 케어 프로토콜</h2>
<p class="section-sub">표면만 닦는 청소가 아닌, 미세 분진과 세균 번식의 원인을 제거하는 체계적인 과정입니다.</p>
<h2 id="protoHeading" class="section-title">6단계 현장 검수 절차 예시</h2>
<p class="section-sub">작업 순서와 확인 항목을 기록하는 방식의 예시입니다. 결과나 성능을 보증하지 않습니다.</p>
</div>
<a href="./protocol/index.html" class="action-btn" style="text-decoration: none; padding: 10px 20px; font-size: 14px; background: var(--surface-card); color: var(--accent); border: 1px solid var(--accent);">
나인원한남 ATP 측정 리포트 보기 →
검수 절차 예시 보기 →
</a>
</div>
@ -269,37 +273,37 @@
<div class="protocol-card">
<div class="protocol-num">STEP 01</div>
<h3>건식 초미세 집진</h3>
<p>H14 헤파 필터 흡진기로 벽면, 천장 몰딩, 바닥의 시공 미세분진 및 부유 입자를 99.97% 1차 포집합니다.</p>
<p>벽면, 천장 몰딩, 바닥의 시공 잔여물을 확인하고 흡진 작업 여부를 기록합니다.</p>
</div>
<div class="protocol-card">
<div class="protocol-num">STEP 02</div>
<h3>160℃ 고온 스팀 살균</h3>
<p>배수구, 욕실 타일 틈새, 주방 후드 기름때를 화학 세제 없이 4.0bar 고온 증기로 녹여 살균합니다.</p>
<h3>스팀 장비 설정 확인</h3>
<p>배수구, 욕실 타일 틈새, 주방 후드처럼 확인이 필요한 면과 장비 설정을 작업표에 기록합니다.</p>
</div>
<div class="protocol-card">
<div class="protocol-num">STEP 03</div>
<h3>약알칼리 친환경 분해</h3>
<p>에코서트(ECOCERT) 인증 천연 식물성 세정제를 도포하여 자재 손상 없이 찌든 때를 안전하게 중화합니다.</p>
<h3>표면별 세정 확인</h3>
<p>표면 재질과 사용 제품 정보를 작업표에 기록하고, 적용 전후 상태를 함께 확인합니다.</p>
</div>
<div class="protocol-card">
<div class="protocol-num">STEP 04</div>
<h3>서랍·후드 전면 탈거 세척</h3>
<p>보이지 않는 싱크대 걸레받이, 전등갓, 수납장 서랍 레일을 전면 탈거하여 내측 분진을 완벽 제거합니다.</p>
<p>필요한 분리 작업의 범위와 접근 가능한 안쪽 면을 확인 항목으로 남깁니다.</p>
</div>
<div class="protocol-card">
<div class="protocol-num">STEP 05</div>
<h3>편백 피톤치드 연무 훈증</h3>
<p>순수 제주 편백 오일 연무로 벽지 속 새집증후군 원인 물질(포름알데히드, VOCs)을 중화하고 탈취합니다.</p>
<h3>환기와 마감 확인</h3>
<p>환기와 마감 상태를 확인하고, 사용한 향·마감 제품이 있다면 작업표에 표시합니다.</p>
</div>
<div class="protocol-card">
<div class="protocol-num">STEP 06</div>
<h3>32개 항목 마스터 검수</h3>
<p>현장 팀장과 고객이 함께 32개 체크리스트를 1:1 대조하고, 미흡 지점 발생 시 즉시 재케어 후 인도합니다.</p>
<p>현장 담당자와 의뢰자가 검수 항목을 함께 확인하는 흐름을 예시로 표시합니다.</p>
</div>
</div>
</div>
@ -309,27 +313,27 @@
<section class="equipment-section" id="equipment" aria-labelledby="equipHeading">
<div class="wrap">
<div class="section-head">
<h2 id="equipHeading" class="section-title">전문 위생 장비 및 검수 스펙</h2>
<p class="section-sub">가정용 장비가 아닌 독일 카처 정품 산업용 위생 기기를 전 현장에 투입합니다.</p>
<h2 id="equipHeading" class="section-title">작업 장비와 검수 기록</h2>
<p class="section-sub">현장에서 확인할 장비·필터·제품·마감 항목을 작업표에 표시하는 예시입니다.</p>
</div>
<div class="equip-grid">
<div class="equip-specs">
<div class="spec-item">
<span class="spec-key">고온 스팀 압력</span>
<span class="spec-val">Kärcher SG 4/4 (4.0 bar · 160℃)</span>
<span class="spec-key">장비 기록</span>
<span class="spec-val">사용 장비와 설정값을 작업표에 표시</span>
</div>
<div class="spec-item">
<span class="spec-key">미세먼지 집진 등급</span>
<span class="spec-val">HEPA H14 (0.3㎛ 99.995% 포집)</span>
<span class="spec-key">필터 확인</span>
<span class="spec-val">필터 종류와 교체 상태를 현장에서 확인</span>
</div>
<div class="spec-item">
<span class="spec-key">사용 세제 인증</span>
<span class="spec-val">ECOCERT 100% 생분해 식물성</span>
<span class="spec-key">사용 제품</span>
<span class="spec-val">제품명과 적용 표면을 작업표에 기록</span>
</div>
<div class="spec-item">
<span class="spec-key">포름알데히드 저감</span>
<span class="spec-val">한국화학융합시험연구원 인증 94.2%</span>
<span class="spec-key">마감 확인</span>
<span class="spec-val">환기·표면 상태를 검수 항목으로 확인</span>
</div>
</div>
@ -341,8 +345,8 @@
height="600"
loading="lazy"
/>
<span class="ba-badge before">정밀 장비 규격</span>
<span class="ba-badge after">QC 완결 보증</span>
<span class="ba-badge before">작업 장비 예시</span>
<span class="ba-badge after">검수 항목 확인</span>
</div>
</div>
</div>
@ -350,16 +354,17 @@
</div>
<!-- ===================================================================
2. 관리자 관제 콘솔 모드 (Admin Dashboard Mockup)
2. 운영 현황 보기
=================================================================== -->
<section class="admin-view wrap" aria-label="청온 케어 관리자 콘솔">
<section class="admin-view wrap" aria-label="청온 케어 운영 현황">
<div class="admin-header">
<div>
<div class="eyebrow" style="margin-bottom: 4px;">배차 관제 콘솔</div>
<h2 style="font-size: var(--step-2); font-weight: 800;">실시간 배차 및 현장 관제 현황</h2>
<div class="eyebrow" style="margin-bottom: 4px;">운영 현황</div>
<h2 style="font-size: var(--step-2); font-weight: 800;">오늘의 케어 현황</h2>
<p class="demo-notice" role="note">아래 접수·배차 정보는 운영 화면 예시입니다. 실제 고객 정보나 접수 내역이 아닙니다.</p>
</div>
<button type="button" class="btn-primary" id="btnOpenQcModal">
32개 현장 검수표 열기
32개 검수 항목 보기
</button>
</div>
@ -386,7 +391,7 @@
<!-- 실시간 접수 및 배차 원장 테이블 -->
<div class="admin-table-card">
<div class="table-header">
<span class="table-title">실시간 예약 및 케어 원장 (Live Ledger)</span>
<span class="table-title">접수·배차 현황</span>
<div class="table-filter-tabs">
<button class="filter-tab is-active" data-filter="all">전체</button>
<button class="filter-tab" data-filter="new">신규접수</button>
@ -399,14 +404,14 @@
<table class="admin-table">
<thead>
<tr>
<th>접수번호</th>
<th>고객명 / 연락처</th>
<th>공간 / 면적</th>
<th>현장 주소</th>
<th>견적액</th>
<th>배정 팀장</th>
<th>상태</th>
<th>조치</th>
<th scope="col">접수번호</th>
<th scope="col">고객명 / 연락처</th>
<th scope="col">공간 / 면적</th>
<th scope="col">현장 주소</th>
<th scope="col">견적액</th>
<th scope="col">배정 팀장</th>
<th scope="col">상태</th>
<th scope="col">조치</th>
</tr>
</thead>
<tbody id="adminTableBody">
@ -418,7 +423,7 @@
<td class="mono" style="font-weight:700;">432,000원</td>
<td>김정온 마스터</td>
<td><span class="status-pill new">신규 접수</span></td>
<td><button class="action-btn" onclick="alert('배차 확인 알림톡이 고객님께 전송되었습니다.');">배차 확정</button></td>
<td><button class="action-btn" onclick="alert('배차 상태를 확인했습니다. 이 화면의 접수 정보는 예시이며 실제 알림은 전송되지 않습니다.');">배차 상태 보기</button></td>
</tr>
<tr>
<td class="mono">#CO-2026-0818</td>
@ -428,7 +433,7 @@
<td class="mono" style="font-weight:700;">520,000원</td>
<td>박준호 팀장 (2팀)</td>
<td><span class="status-pill progress">스팀 케어 중</span></td>
<td><button class="action-btn" onclick="document.getElementById('qcModal').showModal();">실시간 검수</button></td>
<td><button class="action-btn" onclick="document.getElementById('qcModal').showModal();">검수 항목 보기</button></td>
</tr>
<tr>
<td class="mono">#CO-2026-0817</td>
@ -438,7 +443,7 @@
<td class="mono" style="font-weight:700;">841,000원</td>
<td>최수진 닥터 (1팀)</td>
<td><span class="status-pill done">검수 승인 완료</span></td>
<td><button class="action-btn" onclick="alert('완료 검수 리포트가 PDF로 발행되었습니다.');">리포트 다운</button></td>
<td><button class="action-btn" onclick="alert('검수 리포트 내용을 확인했습니다. 실제 발행은 진행되지 않습니다.');">리포트 보기</button></td>
</tr>
</tbody>
</table>
@ -458,23 +463,22 @@
<li class="qc-item"><input type="checkbox" checked /> <span>02. 창틀 상하좌우 틈새 시멘트 가루 및 찌든때 제거</span></li>
<li class="qc-item"><input type="checkbox" checked /> <span>03. 싱크대 하부 걸레받이 전면 탈거 및 분진 집진</span></li>
<li class="qc-item"><input type="checkbox" checked /> <span>04. 가스레인지/인덕션 상판 및 후드 필터 기름때 고온 스팀 분해</span></li>
<li class="qc-item"><input type="checkbox" checked /> <span>05. 욕실 벽타일 메지 및 배수구 트랩 고온 살균 세척</span></li>
<li class="qc-item"><input type="checkbox" checked /> <span>05. 욕실 벽타일 메지 및 배수구 트랩 스팀 작업 확인</span></li>
<li class="qc-item"><input type="checkbox" checked /> <span>06. 수납장 모든 서랍 완전 탈거 후 레일 틈새 분진 청소</span></li>
<li class="qc-item"><input type="checkbox" checked /> <span>07. 베란다 및 다용도실 바닥 타일 약알칼리 세정</span></li>
<li class="qc-item"><input type="checkbox" /> <span>08. 전 실 편백 피톤치드 훈증 및 공기질 측정기 수치 확인</span></li>
<li class="qc-item"><input type="checkbox" checked /> <span>07. 베란다 및 다용도실 바닥 타일 세정 작업 확인</span></li>
<li class="qc-item"><input type="checkbox" /> <span>08. 전 실 환기와 마감 상태 확인</span></li>
</ul>
<div style="display: flex; justify-content: flex-end; gap: 8px;">
<button type="button" class="action-btn" onclick="document.getElementById('qcModal').close();">닫기</button>
<button type="button" class="btn-primary" style="padding: 6px 14px; font-size: 13px;" onclick="alert('검수 승인이 완료되었습니다.'); document.getElementById('qcModal').close();">최종 검수 서명 승인</button>
<button type="button" class="btn-primary" style="padding: 6px 14px; font-size: 13px;" onclick="alert('검수 항목을 확인했습니다. 이 화면에서 실제 승인 처리는 진행되지 않습니다.'); document.getElementById('qcModal').close();">검수 항목 확인</button>
</div>
</dialog>
<!-- 푸터 (.site-foot SSOT) -->
<footer class="site-foot">
<div class="wrap foot-inner">
<p>청온 케어 · 서울 마포구 마포대로 109 청온랩 4층</p>
<p class="muted">연중무휴 08:00 – 20:00 (사전 예약제)</p>
<p class="muted">care@cheongon.kr</p>
<p>청온 케어 · 위생 케어 안내</p>
<p class="muted">이 페이지의 가격·접수 정보는 예시이며 입력 정보는 저장·전송되지 않습니다.</p>
</div>
</footer>
@ -569,9 +573,9 @@
let submissionCount = 6;
btnSubmit.addEventListener("click", () => {
const name = document.getElementById("clientName").value || "고객님";
const phone = document.getElementById("clientPhone").value || "010-0000-0000";
const address = document.getElementById("clientAddress").value || "서울시 현장 주소";
const name = document.getElementById("clientName").value || "이름 미입력";
const phone = document.getElementById("clientPhone").value || "입력 없음";
const address = document.getElementById("clientAddress").value || "입력 없음";
const pyeong = pyeongSlider.value;
const totalStr = sumTotal.textContent;
@ -593,7 +597,7 @@
`;
tableBody.insertBefore(newRow, tableBody.firstChild);
alert(`[청온 케어] ${name}님의 견적(${totalStr}) 신청이 성공적으로 접수되었습니다.\n관리자 콘솔에 실시간 등록되었습니다.`);
alert(`${name}님의 예상 견적은 ${totalStr}입니다.\n실제 견적·신청은 진행되지 않았으며 입력 정보는 저장하거나 전송하지 않습니다.`);
});
// 모달 열기

View file

@ -3,16 +3,13 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>임상 6단계 위생 프로토콜 & 한남 케이스 리포트 — 청온 케어</title>
<meta name="description" content="청온 케어의 병원급 6단계 정밀 위생 프로토콜 및 ATP 생체 오염도 측정 검증 리포트. 나인원한남 86평형 준공 청소 케이스 스터디 및 32개 정밀 QC 체크리스트." />
<title>6단계 위생 케어 절차 · 예시 리포트 — 청온 케어</title>
<meta name="description" content="청온 케어의 현장 검수 절차와 기록 표시 방식을 소개합니다. 아래 사례와 값은 예시입니다." />
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/webfontworld/naver/MaruBuri.css"
/>
<link rel="stylesheet" href="../styles/fonts.css?v=20260912all" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css"
@ -21,8 +18,9 @@
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap"
/>
<link rel="stylesheet" href="../styles/tokens.css" />
<link rel="stylesheet" href="../styles/main.css" />
<link rel="stylesheet" href="../styles/tokens.css?v=20260912all" />
<link rel="stylesheet" href="../styles/main.css?v=20260912all" />
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%230D9488'/%3E%3Cpath d='M9 16.5l5 5 9-10' stroke='%23ffffff' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/%3E%3C/svg%3E" />
<style>
.breadcrumb-bar {
padding-top: var(--space-4);
@ -96,7 +94,7 @@
font-weight: 800;
}
.atp-val.before {
color: #ef4444;
color: #b91c1c;
}
.atp-val.after {
color: var(--accent);
@ -127,6 +125,7 @@
</style>
</head>
<body>
<a class="skip-link" href="#main-content">본문으로 건너뛰기</a>
<!-- Top Nav (GNB SSOT) -->
<header class="site-head">
<div class="wrap head-inner">
@ -153,28 +152,29 @@
<div class="breadcrumb-bar" style="margin-bottom: var(--space-4); display: flex; align-items: center; gap: var(--space-2); font-family: var(--font-mono); font-size: 12px; color: var(--ink-muted); letter-spacing: 0.04em;">
<a href="../index.html">CHEONG-ON CARE</a>
<span class="sep">/</span>
<span class="current" style="color: var(--ink); font-weight: 600;">CLINICAL HYGIENE PROTOCOL &amp; CASE STUDY</span>
<span class="current" style="color: var(--ink); font-weight: 600;">CARE CHECKLIST &amp; CASE EXAMPLE</span>
</div>
<div class="protocol-hero-head">
<p class="hero-label">HOSPITAL-GRADE SANITATION STANDARDS</p>
<h1 class="hero-title" style="margin-bottom: var(--space-2);">단순한 청소를 넘어<br /><span style="color: var(--accent);">병원급 6단계 임상 위생 프로토콜</span></h1>
<p class="hero-label">CARE CHECKLIST EXAMPLE</p>
<h1 class="hero-title" style="margin-bottom: var(--space-2);"><span style="color: var(--accent);">6단계 위생 케어 절차</span></h1>
<p class="hero-lead" style="font-size: var(--step-0); color: var(--ink-muted); max-width: var(--measure); margin-bottom: var(--space-5);">
독일 카처 산업용 스팀과 H14 헤파 필터 흡진, ATP 생체 오염도 계측을 통해 인체에 무해한 안심 공간을 과학적으로 보증합니다.
현장 장비, 필터, 표면, 마감 상태를 작업표에 기록하는 절차와 확인 항목을 소개합니다.
</p>
<p class="demo-notice" role="note">안내: 아래 절차와 측정 수치는 예시입니다. 실제 시공 결과를 보장하거나 예약을 접수하지 않습니다.</p>
<dl class="protocol-specs" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: var(--space-4); background: var(--surface-card); border: 1px solid var(--line); border-radius: var(--radius); padding: var(--space-4) var(--space-5); margin-bottom: var(--space-6);">
<div>
<dt class="spec-label" style="font-family: var(--font-mono); font-size: 11px; color: var(--accent); letter-spacing: 0.02em; margin-bottom: 2px;">Steam Temp</dt>
<dd class="spec-val" style="font-size: var(--step-0); font-weight: 800; color: var(--ink);">160℃ High-Temp</dd>
<dd class="spec-val" style="font-size: var(--step-0); font-weight: 800; color: var(--ink);">현장 장비 설정 기록</dd>
</div>
<div>
<dt class="spec-label" style="font-family: var(--font-mono); font-size: 11px; color: var(--accent); letter-spacing: 0.02em; margin-bottom: 2px;">Filtration</dt>
<dd class="spec-val" style="font-size: var(--step-0); font-weight: 800; color: var(--ink);">0.3㎛ HEPA H14 (99.995%)</dd>
<dd class="spec-val" style="font-size: var(--step-0); font-weight: 800; color: var(--ink);">필터 종류·상태 확인</dd>
</div>
<div>
<dt class="spec-label" style="font-family: var(--font-mono); font-size: 11px; color: var(--accent); letter-spacing: 0.02em; margin-bottom: 2px;">Detergent</dt>
<dd class="spec-val" style="font-size: var(--step-0); font-weight: 800; color: var(--ink);">ECOCERT 100% 식물성</dd>
<dd class="spec-val" style="font-size: var(--step-0); font-weight: 800; color: var(--ink);">사용 제품·표면 기록</dd>
</div>
<div>
<dt class="spec-label" style="font-family: var(--font-mono); font-size: 11px; color: var(--accent); letter-spacing: 0.02em; margin-bottom: 2px;">QC Points</dt>
@ -186,49 +186,48 @@
<figure class="hero-photo" style="border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; background: var(--surface-card);">
<img
src="../assets/clean-hero.webp"
alt="완벽하게 정돈되고 살균된 프리미엄 주거 인테리어 공간"
alt="정돈된 주거 공간의 실내 전경"
width="1600"
height="900"
style="width: 100%; aspect-ratio: 16/9; object-fit: cover;"
/>
<figcaption style="padding: 10px 16px; font-family: var(--font-mono); font-size: 12px; color: var(--ink-muted); border-top: 1px solid var(--line); background: var(--surface-raised);">
나인원한남 86평형 · 6단계 정밀 프로토콜 시공 완료
주거 공간 · 6단계 절차 예시
</figcaption>
</figure>
</div>
</section>
<!-- ATP Measurement Case Report -->
<!-- Inspection Record Layout -->
<section class="wrap" style="padding-bottom: var(--space-8);">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: var(--space-8); align-items: center;">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr)); gap: var(--space-8); align-items: center;">
<div class="frame">
<img
src="../assets/atp-tester.webp"
alt="ATP 생체 오염도 측정기 계측 화면 (12 RLU)"
alt="검수 기록에 사용하는 계측 장비 화면 예시"
style="width: 100%; display: block;"
/>
</div>
<div style="display: flex; flex-direction: column; gap: var(--space-4);">
<div class="eyebrow">ATP 데이터 검증</div>
<div class="eyebrow">검수 기록 표시 예시</div>
<h2 style="font-size: var(--step-2); font-weight: 800; line-height: 1.25;">
나인원한남 86평형<br />
<span style="color: var(--accent);">ATP 생체 오염도 99.1% 저감 리포트</span>
주거 공간 예시<br />
<span style="color: var(--accent);">점검값 기록 방식</span>
</h2>
<p style="font-size: var(--step--1); color: var(--ink-muted); line-height: 1.7;">
실제 한남동 나인원한남 입주 청소 현장에서 주방 싱크대 상판 및 욕실 수전 부위의 미생물 오염도(ATP RLU)를 측정한 결과입니다.
160℃ 고온 스팀과 에코서트 효소 분해 세정 후 병원 무균실 기준(30 RLU 이하)을 상회하는 12 RLU 수준으로 완벽하게 저감되었습니다.
아래 값은 점검 기록이 표시되는 방식을 설명하기 위한 예시입니다. 실제 현장 측정값이나 시공 결과를 뜻하지 않습니다.
</p>
<div class="atp-compare-box">
<div>
<div style="font-family: var(--font-mono); font-size: var(--step--1); color: var(--ink-muted); margin-bottom: 4px;">시공 전 (BEFORE)</div>
<div class="atp-val before">1,420 <span style="font-size: 14px;">RLU</span></div>
<div style="font-size: 12px; color: #ef4444; margin-top: 4px;">심각한 미세 세균 번식</div>
<div style="font-family: var(--font-mono); font-size: var(--step--1); color: var(--ink-muted); margin-bottom: 4px;">점검 전 기록</div>
<div class="atp-val before">기록값 A</div>
<div style="font-size: 12px; color: #b91c1c; margin-top: 4px;">확인할 항목 표시 예시</div>
</div>
<div style="border-left: 1px solid var(--line);">
<div style="font-family: var(--font-mono); font-size: var(--step--1); color: var(--ink-muted); margin-bottom: 4px;">시공 후 (AFTER)</div>
<div class="atp-val after">12 <span style="font-size: 14px;">RLU</span></div>
<div style="font-size: 12px; color: var(--accent); margin-top: 4px;">병원 수술실 기준 패스 (99.1% 저감)</div>
<div style="font-family: var(--font-mono); font-size: var(--step--1); color: var(--ink-muted); margin-bottom: 4px;">점검 후 기록</div>
<div class="atp-val after">기록값 B</div>
<div style="font-size: 12px; color: var(--accent); margin-top: 4px;">확인 완료 표시 예시</div>
</div>
</div>
</div>
@ -239,16 +238,16 @@
<section class="wrap" style="padding-bottom: var(--space-8);">
<div class="eyebrow" style="text-align: center;">단계별 시공 절차</div>
<h2 style="font-size: var(--step-3); font-weight: 800; text-align: center; margin-top: 6px;">
청온 케어 임상 6단계 시공 체계
청온 케어 6단계 검수 절차 예시
</h2>
<div class="step-deep-list">
<div class="step-deep-item">
<div style="font-family: var(--font-mono); font-size: var(--step-2); font-weight: 800; color: var(--accent);">STEP 01</div>
<div>
<h3 style="font-size: var(--step-1); font-weight: 700;">H14 헤파 필터 건식 초미세 흡진</h3>
<h3 style="font-size: var(--step-1); font-weight: 700;">필터 흡진 작업 확인</h3>
<p style="font-size: var(--step--1); color: var(--ink-muted); margin-top: 6px; line-height: 1.6;">
벽면 실크벽지 미세 분진, 천장 우물 몰딩, 조명 레일 내부의 0.3㎛ 부유 입자를 99.995% 집진하여 공기 중 재비산을 완벽 차단합니다.
벽면, 천장 몰딩, 조명 레일의 작업 범위를 확인하고 흡진 작업 여부를 작업표에 기록합니다.
</p>
</div>
</div>
@ -256,9 +255,9 @@
<div class="step-deep-item">
<div style="font-family: var(--font-mono); font-size: var(--step-2); font-weight: 800; color: var(--accent);">STEP 02</div>
<div>
<h3 style="font-size: var(--step-1); font-weight: 700;">160℃ 4.0bar 고온 스팀 살균</h3>
<h3 style="font-size: var(--step-1); font-weight: 700;">스팀 장비 설정 확인</h3>
<p style="font-size: var(--step--1); color: var(--ink-muted); margin-top: 6px; line-height: 1.6;">
배수구 유가, 변기 틈새, 주방 후드 필터에 화학 세제 없이 4.0bar 고압 증기를 분사하여 기름때를 녹이고 99.9% 멸균합니다.
배수구, 욕실 틈새, 주방 후드처럼 확인이 필요한 면을 구분하고 사용 장비 설정을 남깁니다.
</p>
</div>
</div>
@ -266,9 +265,9 @@
<div class="step-deep-item">
<div style="font-family: var(--font-mono); font-size: var(--step-2); font-weight: 800; color: var(--accent);">STEP 03</div>
<div>
<h3 style="font-size: var(--step-1); font-weight: 700;">약알칼리 에코서트 친환경 효소 분해</h3>
<h3 style="font-size: var(--step-1); font-weight: 700;">표면별 세정 확인</h3>
<p style="font-size: var(--step--1); color: var(--ink-muted); margin-top: 6px; line-height: 1.6;">
원목 마루 및 천연 대리석 자재에 손상을 주지 않는 100% 생분해성 식물 세정제를 도포하여 찌든 산화 피막을 안전하게 중화합니다.
바닥과 석재의 재질을 확인한 뒤 사용 제품과 적용 범위를 작업표에 표시합니다.
</p>
</div>
</div>
@ -278,7 +277,7 @@
<div>
<h3 style="font-size: var(--step-1); font-weight: 700;">서랍장·걸레받이 전면 탈거 정밀 세척</h3>
<p style="font-size: var(--step--1); color: var(--ink-muted); margin-top: 6px; line-height: 1.6;">
싱크대 하부 걸레받이와 수납장 레일을 전량 분리 탈거하여, 보이지 않는 시공 톱밥 및 유해 분진을 내측까지 완전 제거합니다.
싱크대 하부와 수납장 레일의 접근 가능 여부를 확인하고, 분리 작업 범위를 기록합니다.
</p>
</div>
</div>
@ -286,9 +285,9 @@
<div class="step-deep-item">
<div style="font-family: var(--font-mono); font-size: var(--step-2); font-weight: 800; color: var(--accent);">STEP 05</div>
<div>
<h3 style="font-size: var(--step-1); font-weight: 700;">제주 편백 피톤치드 연무 훈증</h3>
<h3 style="font-size: var(--step-1); font-weight: 700;">환기와 마감 확인</h3>
<p style="font-size: var(--step--1); color: var(--ink-muted); margin-top: 6px; line-height: 1.6;">
새집증후군 유발 물질인 포름알데히드 및 휘발성유기화합물(VOCs)을 중화하고 실내 숲속 공기질을 복원합니다.
환기와 마감 상태를 확인하고, 향·마감 제품을 사용했다면 제품명과 적용 범위를 기록합니다.
</p>
</div>
</div>
@ -296,9 +295,9 @@
<div class="step-deep-item">
<div style="font-family: var(--font-mono); font-size: var(--step-2); font-weight: 800; color: var(--accent);">STEP 06</div>
<div>
<h3 style="font-size: var(--step-1); font-weight: 700;">32개 항목 마스터 닥터 1:1 검수 및 디지털 인증</h3>
<h3 style="font-size: var(--step-1); font-weight: 700;">32개 항목 현장 검수 확인</h3>
<p style="font-size: var(--step--1); color: var(--ink-muted); margin-top: 6px; line-height: 1.6;">
배차 현장 팀장과 고객이 함께 32개 정밀 체크리스트를 1:1 대조하고, 모바일 리포트를 발행하여 최종 승인합니다.
현장 담당자와 의뢰자가 32개 체크리스트를 함께 확인하는 흐름을 예시로 표시합니다.
</p>
</div>
</div>
@ -310,7 +309,7 @@
<div class="frame">
<img
src="../assets/steam-qc.webp"
alt="독일 카처 4.0bar 정밀 스팀 위생 장비와 멸균 키트"
alt="작업 장비와 검수 키트를 보여 주는 예시 이미지"
style="width: 100%; display: block; aspect-ratio: 16/9; object-fit: cover;"
/>
</div>
@ -324,10 +323,10 @@
우리 집 공간의 정찰제 견적을 확인하세요
</h2>
<p style="font-size: var(--step--1); color: var(--ink-muted); margin: 12px 0 24px;">
추가금 없는 평당 11,000원 명확한 기준. 1분 만에 실시간 견적 산출이 가능합니다.
선택 항목을 기준으로 예상 금액을 확인할 수 있습니다. 실제 견적이나 신청은 진행되지 않습니다.
</p>
<a href="../index.html#quote" class="btn-primary" style="display: inline-block; padding: 14px 28px; text-decoration: none; font-size: var(--step-0);">
실시간 정찰제 견적 계산하기 →
<a href="../index.html#calculator" class="btn-primary" style="display: inline-block; padding: 14px 28px; text-decoration: none; font-size: var(--step-0);">
항목별 예상 견적 보기 →
</a>
</div>
</section>
@ -336,9 +335,8 @@
<!-- 푸터 (.site-foot SSOT) -->
<footer class="site-foot">
<div class="wrap foot-inner">
<p>청온 케어 · 서울 마포구 마포대로 109 청온랩 4층</p>
<p class="muted">연중무휴 08:00 – 20:00 (사전 예약제)</p>
<p class="muted">care@cheongon.kr</p>
<p>청온 케어 · 위생 케어 안내</p>
<p class="muted">이 페이지의 절차와 수치는 예시이며 실제 예약은 접수되지 않습니다.</p>
</div>
</footer>
</body>

View file

@ -0,0 +1,15 @@
@font-face {
font-family: "MaruBuri";
src: url("../assets/fonts/MaruBuri-Regular.woff2") format("woff2");
font-style: normal;
font-weight: 400;
font-display: swap;
}
@font-face {
font-family: "MaruBuri";
src: url("../assets/fonts/MaruBuri-SemiBold.woff2") format("woff2");
font-style: normal;
font-weight: 600;
font-display: swap;
}

View file

@ -1,4 +1,4 @@
@import "./tokens.css";
@import "./tokens.css?v=20260912all";
/* ==========================================================================
청온 케어 (CHEONG-ON CARE) — 메인 스타일시트 SSOT
@ -193,7 +193,7 @@ button {
display: inline-flex;
align-items: center;
gap: 5px;
transition: all var(--dur-quick);
transition: background-color var(--dur-quick), color var(--dur-quick), box-shadow var(--dur-quick);
white-space: nowrap;
}
@ -296,7 +296,7 @@ button {
.hero-grid {
display: grid;
grid-template-columns: minmax(0, 1.25fr) minmax(0, 0.95fr);
grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
gap: var(--space-7);
align-items: center;
}
@ -351,41 +351,20 @@ button {
position: relative;
}
.bezel-outer {
background: rgba(15, 118, 110, 0.05);
border: 1px solid var(--line);
padding: 6px;
border-radius: var(--radius-md);
}
.hero-frame {
border-radius: calc(var(--radius-md) - 4px);
overflow: hidden;
margin: 0;
border: 1px solid var(--line);
box-shadow: 0 16px 40px -12px rgba(0, 0, 0, 0.08);
background: #ffffff;
}
.hero-frame img {
width: 100%;
aspect-ratio: 4/3;
aspect-ratio: 5/4;
object-fit: cover;
display: block;
}
.hero-stat-floating {
position: absolute;
bottom: -14px;
left: 16px;
background: var(--surface-raised);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
padding: 8px 14px;
box-shadow: 0 8px 24px -4px rgba(0, 0, 0, 0.08);
display: flex;
align-items: baseline;
gap: var(--space-2);
}
.hero-frame figcaption { padding: var(--space-3) var(--space-4); border-top: 1px solid var(--line); color: var(--ink-muted); font-size: var(--step--1); line-height: 1.55; }
/* ── 투명 정찰제 실시간 견적 산정 섹션 ── */
.calculator-section {
@ -393,10 +372,10 @@ button {
border-top: 1px solid var(--line);
}
.section-head {
text-align: center;
max-width: 680px;
margin-inline: auto;
.calc-section .section-head {
text-align: left;
max-width: 48rem;
margin-inline: 0;
margin-bottom: var(--space-6);
}
@ -463,7 +442,7 @@ button {
font-weight: 600;
font-size: var(--step--1);
text-align: center;
transition: all var(--dur-quick);
transition: background-color var(--dur-quick), border-color var(--dur-quick), color var(--dur-quick);
white-space: normal;
word-break: keep-all;
}
@ -513,7 +492,7 @@ button {
font-size: var(--step--1);
color: var(--ink-muted);
cursor: pointer;
transition: all var(--dur-quick);
transition: background-color var(--dur-quick), border-color var(--dur-quick), color var(--dur-quick);
}
.addon-label:has(input:checked) {
@ -850,7 +829,7 @@ button {
font-weight: 600;
background: var(--surface);
cursor: pointer;
transition: all var(--dur-quick);
transition: border-color var(--dur-quick), color var(--dur-quick);
}
.action-btn:hover {
@ -864,10 +843,13 @@ button {
inset: 0;
margin: auto;
width: min(90vw, 560px);
max-height: calc(100dvh - (var(--space-4) * 2));
background: #ffffff;
border: 1px solid var(--line);
border-radius: var(--radius-md);
padding: var(--space-5);
overflow: auto;
overscroll-behavior: contain;
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.15);
outline: none;
}
@ -923,3 +905,75 @@ button {
font-size: 11px;
}
}
html:has(dialog[open]) {
overflow: hidden;
}
/* 접근성·초광폭 프리플라이트: 컨트롤의 독립성과 실제 표면 여백을 같은 토큰으로 유지한다. */
:where(a, button, input, select, textarea):focus-visible {
outline: 3px solid var(--accent);
outline-offset: 2px;
}
.btn-primary,
.btn-outline,
.calc-submit,
.type-btn,
.addon-label,
.action-btn,
.filter-tab,
.mode-btn,
.head-actions .btn,
input:not([type="checkbox"]),
select,
textarea {
min-height: var(--control-block);
}
.btn-primary,
.btn-outline,
.calc-submit,
.type-btn,
.addon-label,
.action-btn,
.filter-tab,
.mode-btn,
.head-actions .btn {
padding-inline: var(--control-pad-inline);
}
.form-label + input,
.form-label + select,
.form-label + textarea {
padding-inline: var(--control-pad-inline);
}
.demo-notice {
max-width: 58ch;
margin-top: var(--space-4);
padding: var(--space-3) var(--space-4);
border-left: 3px solid var(--accent);
background: var(--accent-light);
color: var(--ink-muted);
font-size: var(--step--1);
line-height: 1.55;
word-break: keep-all;
}
@media (max-width: 600px) {
.head-actions .client-only-btn { display: none; }
.hero-ctas { align-items: stretch; }
.hero-ctas .btn-primary { width: 100%; }
.hero-title { font-size: clamp(1.65rem, 7.8vw, 2.15rem); }
.hero-trust { align-items: flex-start; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 1ms !important;
animation-iteration-count: 1 !important;
scroll-behavior: auto !important;
transition-duration: 1ms !important;
}
}

View file

@ -1,7 +1,7 @@
/* ==========================================================================
청온 케어 (CHEONG-ON CARE) — 디자인 토큰 SSOT
프리셋: ledger-console + swiss-minimal
컨셉: 임상 수준의 위생 프로토콜과 정직한 정찰제 — 공간의 숨결을 되돌리는 케어
컨셉: 현장 검수 절차와 정직한 정찰제 — 공간의 상태를 기록하는 케어
단일 강조색: 에메랄드 틸 (#0D9488)
========================================================================== */
@ -42,8 +42,8 @@
--line-inverse: #334155;
/* 단일 강조색 (에메랄드 틸) */
--accent: #0d9488;
--accent-hover: #0f766e;
--accent: #0f766e;
--accent-hover: #115e59;
--accent-light: #f0fdfa;
--accent-surface: #ccfbf1;
--accent-ink: #ffffff;
@ -63,8 +63,10 @@
--space-8: clamp(4.5rem, 3rem + 6vw, 9rem);
/* 컨테이너 및 패딩 — 전체 쇼케이스 절대 통일 SSOT */
--container: 1120px;
--container: clamp(70rem, 62vw, 85rem);
--pad-inline: clamp(1.25rem, 1rem + 2.5vw, 4rem);
--control-block: 2.75rem;
--control-pad-inline: 0.875rem;
/* 모션 */
--dur-quick: 180ms;

View file

@ -0,0 +1,24 @@
# 두레대학 수강신청 디자인 기록
## 브리프
- 무엇을: 과목 목록과 내 시간표를 한 작업면에서 비교하며 신청하는 화면.
- 누구에게: 과목명·요일·학점·여석과 신청 결과를 대조하는 학생.
- 제약: 과목·정원 데이터, 신청 계산, 취소·실행취소, 단축키, 인쇄와 모바일 하단 내비를 보존한다.
## 판단 기준
| 축 | 질문 | 적용 |
| --- | --- | --- |
| Warde · 정보 전달 | 필터 뒤의 과목 목록과 시간표가 한 작업 흐름으로 읽히는가 | 공통 작업면과 한 단계 낮춘 중복 테두리 |
| Ruder · 비교 변수 | 과목명·시간·학점·여석을 같은 시선 흐름에서 비교하는가 | 행 메타와 우측 학점·여석의 정렬, tabular figures |
| Albers · 상태 맥락 | 관심·신청·여석 부족 상태가 텍스트와 색으로 함께 구분되는가 | 상태 텍스트를 남기고 색은 상태 보조로만 사용 |
Warde, Ruder, Albers는 정보 전달·비교 변수·상태 맥락을 검토하는 참고 질문이며 인과나 성능 법칙이 아니다.
## 역할 토큰과 검증
- `--workspace-radius`, `--workspace-rule`, `--workspace-body-size`, `--workspace-meta-size`가 작업면과 정보 위계를 관리한다.
- `verify-final` 18개 묶음은 16개가 즉시 통과했다. 전체 묶음의 invariant·visual 검사는 Gaon·Dure를 대상으로 하며, 나머지 2개는 수정 뒤 release 독립 재실행에서 ALL PASS·ALL MATCH였다.
- root는 28개 모바일, 21개 데스크톱, 7개 하위 경로와 30개 앱 상태를 직접 검토해 수용했다. stress 112건은 hard failure 0이며, 가로 넘침 4건은 targeted fix 뒤 390px 재측정에서 0이다.
- 이 기록은 렌더·계약 범위의 현재 결과다. 모든 폭·키보드 경로·인쇄 등 개별 기능을 완전 재실행했다는 뜻이 아니며, 참여자 과업 효과도 미측정이다.

View file

@ -17,7 +17,7 @@
<meta name="twitter:image" content="https://designpaca.chanpaca.net/work/dure-enrollment/og.png" />
<meta
name="description"
content="과목을 고르면 시간표에 얹히는 수강신청 데모. 관심 과목, 신청 내역, 이수 현황까지 여섯 화면. 시간 겹침과 학점 초과를 실제로 검사합니다."
content="과목 선택과 시간표를 함께 살펴보는 수강신청 화면입니다. 관심 과목, 신청 내역, 이수 현황을 한 흐름으로 확인할 수 있습니다."
/>
<meta property="og:title" content="두레대학 수강신청 · 시간표" />
<meta property="og:description" content="시간표에 얹히는 수강신청." />
@ -35,7 +35,7 @@
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=IBM+Plex+Sans+KR:wght@400;500;600&family=Noto+Serif+KR:wght@500;600&display=swap"
/>
<link rel="stylesheet" href="./styles/main.css?v=23" />
<link rel="stylesheet" href="./styles/main.css?v=20260912all" />
<link
rel="icon"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%23fafafa'/%3E%3Crect x='6.5' y='6.5' width='19' height='19' rx='4' fill='%23c2410c'/%3E%3Crect x='9.5' y='10' width='5.5' height='4.5' rx='1' fill='%23ffffff'/%3E%3Crect x='16' y='10' width='6.5' height='4.5' rx='1' fill='%23ffffff' opacity='0.8'/%3E%3Crect x='9.5' y='15.8' width='5.5' height='9.2' rx='1' fill='%23ffffff' opacity='0.9'/%3E%3Crect x='16' y='15.8' width='6.5' height='6.2' rx='1' fill='%23ffffff' opacity='0.75'/%3E%3C/svg%3E"
@ -77,7 +77,8 @@
<section class="view is-on" id="view-catalog" aria-labelledby="cat-h" tabindex="-1">
<section class="catalog" aria-labelledby="cat-h">
<h2 class="v-h" id="cat-h">2026학년도 1학기 과목</h2>
<p class="view-sub">과목을 누르면 강의계획과 신청 버튼이 열립니다. 신청하면 시간표에 얹힙니다.</p>
<p class="view-sub">과목을 누르면 강의계획과 신청 버튼이 열립니다. 선택한 과목은 시간표에서 확인할 수 있습니다.</p>
<p class="view-sub" role="note">이 페이지의 과목·정원·이수 현황은 예시입니다. 선택한 과목은 새로고침하면 초기화됩니다.</p>
<div class="sheet">
<div class="toolbar" role="search">
@ -117,7 +118,7 @@
<!-- ── 관심 과목 ── -->
<section class="view" id="view-starred" aria-labelledby="st-h" tabindex="-1">
<h2 class="v-h" id="st-h">관심 과목</h2>
<p class="view-sub">강의계획에서 관심을 남기면 여기에 모입니다. 여석과 시간은 실시간입니다.</p>
<p class="view-sub">강의계획에서 관심을 남기면 여기에 모입니다. 여석과 시간은 과목 정보에서 확인하세요.</p>
<div class="statline" id="st-stats"></div>
<p class="st-warn" id="st-warn" role="status" hidden></p>
<div class="toolbar">
@ -324,7 +325,7 @@
/*
수강신청 상태 기계 — 겹침 검사, 학점 한도, 여석 감소까지 전부 진짜.
여섯 화면이 같은 상태(enrolled, starred)를 공유한다.
새로고침하면 초기화됩니다(데모 원칙: 저장을 흉내내지 않는다).
새로고침하면 초기화하며, 선택 내용은 저장하지 않는다.
*/
const fmtDay = (d) => ({ 1: "월", 2: "화", 3: "수", 4: "목", 5: "금" })[d];
@ -344,14 +345,14 @@
const NOTICES = [
{ date: "03-08", kind: "변경", body: "게임이론(ECON315) 강의실이 경상관 308호에서 309호로 바뀝니다. 이 목록에는 이미 반영했습니다." },
{ date: "03-07", kind: "공지", body: "수강신청 기간은 3월 2일부터 3월 13일 자정까지입니다. 기간 끝에는 시간표가 확정됩니다." },
{ date: "03-06", kind: "안내", body: "기준 학점(18)을 넘는 신청은 학부 사무실의 승인을 받아야 합니다. 이 데모에서는 넘는 신청을 막아둡니다." },
{ date: "03-06", kind: "안내", body: "기준 학점(18)을 넘는 신청은 학부 사무실의 승인을 받아야 합니다. 이 화면에서는 기준을 넘는 신청을 막습니다." },
{ date: "03-05", kind: "안내", body: "드로잉 입문(ART130)은 재료비 3만 원이 듭니다. 첫 수업에 안내합니다." },
{ date: "03-03", kind: "공지", body: "정원이 마감된 과목은 대기자 신청을 받지 않습니다. 분반 개설은 학부 상황에 따라 결정됩니다." },
{ date: "03-02", kind: "공지", body: "2026학년도 1학기 수강신청이 열렸습니다. 시간이 겹치는 과목은 함께 담을 수 없습니다." },
];
const MAX_CREDITS = 18;
// 네 학기를 마친 시점의 이수 기록 — 이 데모의 가상 학생
// 네 학기를 마친 시점의 이수 기록 — 화면에 표시하는 예시 학생
const RECORD = [
{ label: "전공", need: 66, done: 45 },
{ label: "교양", need: 24, done: 21 },
@ -868,7 +869,7 @@
if (!current || $("cd-apply").disabled) return;
current.seats -= 1;
enrolled.add(current.id);
setStatus(`${current.name} 신청 완료 · ${timeLabel(current)} · ${credits()}학점`, false);
setStatus(`${current.name} 시간표에 추가됨 · ${timeLabel(current)} · ${credits()}학점`, false);
dialog.close();
renderAll();
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Before After
Before After

View file

@ -141,8 +141,9 @@ h3 { font-size: var(--step-0); font-weight: 500; }
max-width: 1180px;
margin-inline: auto;
padding: var(--space-5) var(--space-5) 96px;
min-width: 0;
}
.view { display: none; }
.view { display: none; min-width: 0; }
.view.is-on { display: block; }
.view:focus { outline: none; }
.view > h1 { margin-bottom: 4px; }
@ -152,11 +153,15 @@ h3 { font-size: var(--step-0); font-weight: 500; }
.view.is-on { animation: view-in 70ms var(--ease-out) both; }
@keyframes view-in { from { opacity: 0; transform: translateY(4px); } }
}
.view-sub { color: var(--muted-foreground); font-size: var(--step-0); margin-bottom: var(--space-5); max-width: 46em; }
.view-sub { color: var(--muted-foreground); font-size: var(--workspace-meta-size); margin-bottom: var(--space-5); max-width: 46em; }
#view-catalog.is-on {
display: grid;
grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
gap: var(--space-4);
gap: 0;
background: var(--card);
border: 1px solid var(--workspace-rule);
border-radius: var(--workspace-radius);
box-shadow: none;
align-items: start;
}
.view > *:not(:first-child):not(.view-sub) { margin-top: 0; }
@ -226,16 +231,17 @@ h3 { font-size: var(--step-0); font-weight: 500; }
appearance: none; background: none; cursor: pointer; font: inherit;
width: 100%; text-align: left;
display: flex; justify-content: space-between; align-items: center; gap: var(--space-4);
padding: var(--space-3) var(--space-2);
min-width: 0;
padding: 0.875rem var(--space-2);
border: 0; border-bottom: 1px solid var(--border);
border-radius: var(--radius);
border-radius: 0;
transition: background-color 80ms ease;
}
.course-row:first-child { border-top: 1px solid var(--border); }
.course-row:hover { background: var(--muted); }
.course-row:active { scale: 0.995; }
.c-main { display: grid; gap: 2px; min-width: 0; }
.c-name { font-weight: 600; display: flex; align-items: baseline; gap: var(--space-2); flex-wrap: wrap; }
.c-name { font-weight: 600; display: flex; align-items: baseline; gap: var(--space-2); flex-wrap: wrap; font-size: var(--workspace-body-size); }
.c-dept {
font-size: var(--step--1); font-weight: 500;
color: var(--muted-foreground);
@ -243,10 +249,10 @@ h3 { font-size: var(--step-0); font-weight: 500; }
border-radius: var(--radius-xs);
padding: 1px 6px;
}
.c-sub { color: var(--muted-foreground); font-size: var(--step--1); }
.c-side { display: grid; gap: 2px; text-align: right; flex: none; font-size: var(--step--1); }
.c-credit { font-weight: 500; color: var(--foreground); }
.c-seats { color: var(--muted-foreground); }
.c-sub { color: var(--muted-foreground); font-size: var(--workspace-meta-size); }
.c-side { display: grid; min-width: 4.75rem; gap: 0.25rem; text-align: right; flex: none; font-size: var(--workspace-meta-size); }
.c-credit { font-weight: 500; color: var(--foreground); font-variant-numeric: tabular-nums; }
.c-seats { color: var(--muted-foreground); font-variant-numeric: tabular-nums; }
.c-seats.is-low { color: var(--danger); font-weight: 600; }
.c-star { color: var(--accent); font-weight: 600; }
.c-enrolled {
@ -262,8 +268,11 @@ h3 { font-size: var(--step-0); font-weight: 500; }
.empty { padding: var(--space-4) 0; color: var(--muted-foreground); }
/* ── 표 ── */
.table-wrap { overflow-x: auto; overflow-y: hidden; }
table { width: 100%; border-collapse: collapse; }
.table-wrap { max-width: 100%; overflow-x: auto; overflow-y: hidden; overscroll-behavior-inline: contain; scrollbar-gutter: stable both-edges; }
table { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
.table-wrap > table { min-width: 600px; }
#en-table, #ar-table { min-width: 680px; }
table th, table td { overflow-wrap: normal; word-break: keep-all; }
th { font-weight: 500; } /* UA 기본 th 볼드 재정의 */
thead th {
text-align: left; font-weight: 500; font-size: var(--step--1);
@ -289,7 +298,7 @@ tbody tr:hover { background: var(--muted); }
background: none; color: var(--muted-foreground);
border: 1px solid var(--border-strong); border-radius: var(--radius);
height: 28px; padding: 0 10px;
transition: all 100ms ease;
transition: background-color 100ms ease, border-color 100ms ease, color 100ms ease;
white-space: nowrap;
}
.give-btn:hover { border-color: var(--danger); color: var(--danger); }
@ -403,6 +412,8 @@ tbody tr:hover { background: var(--muted); }
/* ── 모달: 스플릿. 정보 패널과 핵심 동작을 분리한다. ── */
.modal {
position: fixed;
inset: 0;
margin: auto;
width: min(94vw, 860px);
max-height: min(86dvh, 760px);
@ -412,8 +423,11 @@ tbody tr:hover { background: var(--muted); }
background: var(--card);
color: var(--foreground);
overflow: hidden;
overscroll-behavior: contain;
box-shadow: var(--shadow-xl);
}
html:has(dialog.modal[open]) { overflow: hidden; }
.modal::backdrop { background: var(--backdrop-dim); backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px); }
@media (prefers-reduced-motion: no-preference) {
.modal[open] { animation: modal-in 200ms cubic-bezier(0.16, 1, 0.3, 1) both; }
@ -514,7 +528,8 @@ tbody tr:hover { background: var(--muted); }
/* ── 반응형 ── */
@media (max-width: 900px) {
#view-catalog.is-on { grid-template-columns: minmax(0, 1fr); gap: var(--space-4); }
#view-catalog.is-on { grid-template-columns: minmax(0, 1fr); gap: 0; }
#view-catalog .catalog { border-right: 0; border-bottom: 1px solid var(--workspace-rule); }
.timetable-panel { position: static; }
.notice-list li { grid-template-columns: minmax(0, 1fr); gap: var(--space-1); }
.no-kind, .no-date { justify-self: start; }
@ -526,6 +541,10 @@ tbody tr:hover { background: var(--muted); }
.layout {
padding: var(--space-4) max(var(--space-5), var(--safe-right)) calc(72px + var(--safe-bottom)) max(var(--space-5), var(--safe-left));
}
.toolbar { align-items: stretch; min-width: 0; }
#q { width: 100%; min-width: 0; flex: 1 1 100%; }
.chips { width: 100%; min-width: 0; flex-wrap: wrap; }
.chip { min-width: 0; flex: 1 1 0; justify-content: center; }
}
@media (max-width: 680px) {
.mspread { grid-template-columns: minmax(0, 1fr); }
@ -675,15 +694,15 @@ kbd {
border-top: 1px solid var(--border);
padding: 6px max(var(--space-2), var(--safe-left)) calc(6px + var(--safe-bottom)) max(var(--space-2), var(--safe-right));
}
@media (max-width: 900px) { .bottom-nav { display: grid; grid-auto-flow: column; } }
@media (max-width: 900px) { .bottom-nav { display: grid; min-width: 0; grid-template-columns: repeat(5, minmax(0, 1fr)); grid-auto-flow: row; } }
.bnav-item {
appearance: none; background: none; border: 0; cursor: pointer; font: inherit;
display: grid; justify-items: center; align-content: center; gap: 3px;
min-height: 52px; min-width: 48px; padding: 4px 2px;
min-height: 52px; min-width: 0; padding: 4px 2px;
border-radius: var(--radius);
color: var(--muted-foreground);
}
.bnav-item span { font-size: var(--step--2); font-weight: 500; }
.bnav-item span { max-width: 100%; font-size: var(--step--2); font-weight: 500; overflow-wrap: anywhere; }
.bnav-item.is-current { color: var(--accent-ink); font-weight: 600; }
.bnav-item.is-current svg { color: var(--accent); }

View file

@ -77,6 +77,11 @@
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
--workspace-radius: 8px;
--workspace-rule: var(--border);
--workspace-body-size: 0.9375rem;
--workspace-meta-size: 0.8125rem;
/* 안전 영역 — viewport-fit=cover 선언 후에만 0이 아닌 값이 온다(WebKit 공식·MDN env()) */
--safe-top: env(safe-area-inset-top, 0px);
--safe-bottom: env(safe-area-inset-bottom, 0px);

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,13 @@
# MaruBuri source record
Downloaded 2026-09-12 from the official NAVER Hangeul static host. The files are retained unchanged: no conversion or subsetting was performed.
| File | Official URL | HTTP content type | Size | SHA-256 |
| --- | --- | --- | --- | --- |
| `MaruBuri-Regular.woff2` | https://hangeul.pstatic.net/hangeul_static/webfont/MaruBuri/MaruBuri-Regular.woff2 | `font/woff2` | 433776 bytes | `4CF1341CF2F23FB3E263712DFDE1D8F25EEDCC328B696A2E5A2C8ADD55E5C17B` |
| `MaruBuri-SemiBold.woff2` | https://hangeul.pstatic.net/hangeul_static/webfont/MaruBuri/MaruBuri-SemiBold.woff2 | `font/woff2` | 466108 bytes | `7AEF4862FA3219B0A7DEB6746E27116A38C4069F4424EB51EDF103B29795F5C2` |
- Official stylesheet checked: https://hangeul.pstatic.net/hangeul_static/css/maru-buri.css
- Official NAVER license page saved verbatim as `LICENSE.html`: https://help.naver.com/service/11029/contents/18088?lang=ko&osType=PC
- `LICENSE.html` SHA-256: `8538F7921744D5E040F603BC301465EFBD038F1054D0754B7508DE3286452EC3`
- The saved official page includes the NAVER copyright notice and the SIL Open Font License 1.1 text for MaruBuri.

View file

@ -2,7 +2,7 @@
## 브리프
- **무엇을**: 가상 서체 기획을 정직하게 밝히면서, 실제 마루 부리와 Pretendard로 문장 조판의 차이를 즉시 체험하는 한국어 타입 스튜디오.
- **무엇을**: 가상 서체 기획을 정직하게 밝히면서, 실제 마루 부리와 Pretendard로 문장 조판의 형태 차이를 즉시 체험하는 한국어 타입 스튜디오.
- **누구에게**: 활자를 고르거나 본문 조판 값을 결정해야 하는 편집자·디자이너·개발자.
- **제약**: ko-KR 한국어, 밝은 아이보리·검정 본문·버밀리언 정체성 유지. 가상 서체를 판매하거나 가짜 스펙을 제시하지 않으며, 본문은 최대 42rem. 320px부터 가로 넘침이 없어야 한다.
@ -15,7 +15,7 @@
| `KLIM-FONTS` · R1 구조 | https://klim.co.nz/fonts/ | Display, Text, Narrow, Condensed처럼 같은 계열의 역할과 폭을 나란히 제시한다. | 글꼴을 사양 표가 아니라 읽는 역할로 비교한다. 같은 문장을 두 역할로 렌더해 비교한다. |
| `CHIPPERFIELD-PROJECTS` · R2 톤 | https://davidchipperfield.com/projects | 프로젝트명·연도·필터가 정렬선과 짧은 메타 안에서 읽힌다. | 큰 표본 아래에 기준선, 짧은 메타, 한 가지 행동을 정렬한다. 1440px 첫 화면에서 표본·상태·CTA의 순서를 확인한다. |
| `SANDOLL-TYPELAB` · R3 디테일 | https://typelab.sandoll.co.kr/experience | 권장규격과 맞춤규격, 한글 범위를 선택하며 표본을 실제 작업으로 연결한다. | 버튼과 조판 슬라이더가 즉시 보이는 결과를 바꾸게 한다. 키보드·range·리셋으로 값을 바꾸어 검증한다. |
| `WCAG-READ` · 접근성 | 200% 텍스트 확대와 320 CSS px reflow를 핵심 화면에서 별도로 시험한다. | 큰 활자 장면은 접근성 제목을 대신하지 않는다. | 320px 200% 텍스트에서 문서 폭 320px, h1 80px을 기록하고 장식 표본은 `aria-hidden`으로 둔다. |
| `WCAG-READ` · 접근성 | 200% 텍스트 확대와 320 CSS px reflow를 핵심 화면에서 별도로 시험한다. | 큰 활자 장면은 접근성 제목을 대신하지 않는다. | 이전 판에서 320px·200% 확대와 `aria-hidden` 표본을 관찰했다. Round 4 현재 결과는 최종 proof를 참조한다. |
## 방향
@ -25,11 +25,11 @@
## 토큰과 구현
- **폰트**: 마루 부리는 제목·조판 시험대·표본의 제목 상태, Pretendard는 본문·기본 표본 상태. 두 폰트 모두 CDN 웹폰트이며 `font-synthesis-weight: none`으로 합성 굵기를 막는다.
- **폰트**: 마루 부리는 공식 로컬 WOFF2 Regular 400·SemiBold 600으로 제목·조판 시험대·표본의 제목 상태를 맡고, Pretendard CDN은 본문·기본 표본 상태를 맡는다. `font-synthesis-weight: none`으로 합성 굵기를 막는다.
- **색**: `--surface #f5f1e8`, `--ink #26221d`, `--accent #a63d2f`. 버밀리언은 표본과 행동에만 쓴다.
- **크기**: h1 `clamp(40px, 1.3rem + 4.2vw, 80px)`, 표본 `clamp(96px, 17.5vw, 260px)`, 본문 폭 최대 `42rem`.
- **레이아웃**: 데스크톱 표본 레일 최대 1700px, 일반 콘텐츠 1216px. 모바일은 최소 20px 인셋을 지키고 표본은 한 줄에 남는다.
- **상호작용**: native 버튼 두 개의 `aria-pressed`가 표본 폰트와 상태 문구를 바꾼다. 기존의 크기·자간·행간 range와 리셋은 유지한다.
- **크기**: h1 `clamp(40px, 1.6rem + 3vw, 60px)`, 표본 `clamp(96px, 11vw, 180px)`, 본문 폭 최대 `42rem`.
- **레이아웃**: 데스크톱 첫 화면은 5:7 두 칼럼으로, 종이색 소개면과 적갈색 표본면을 맞물린다. 900px 미만에서는 소개면 다음에 표본면을 한 칼럼으로 둔다.
- **상호작용**: native 버튼 두 개의 `aria-pressed`가 표본 폰트와 상태 문구를 바꾼다. 조판 시험대는 실제 마루 부리·Pretendard 선택과 크기·자간·행간 range를 제공하고, 리셋은 네 값을 함께 복원한다.
- **성능**: 새 JavaScript 라이브러리·이미지·애니메이션 없음. 초기 체험은 인라인 DOM 코드만 사용한다. 활자 케이스의 두 장면은 생성 이미지임을 캡션으로 밝힌다.
## 의도적으로 하지 않은 것
@ -38,10 +38,40 @@
- 인장·붓글씨·세로쓰기·종이결처럼 한국 활자를 이국화하는 장식을 사용하지 않는다. 활자의 형태와 조판만으로 분위기를 만든다.
- 움직임으로 활자의 차이를 설명하지 않는다. reduced motion에서도 같은 표본과 모든 조작이 남는다.
## 검증 기록 · 2026-09-12
## 이전 판 검증 기록 · 2026-09-12
아래는 이전 판에서 남긴 역사적 관찰이며 Round 4의 현재 통과 결과가 아니다. Round 4 결과는 최종 proof를 참조한다.
- `outputs/design-expertise-20260912/font/font-redesign-e2e.mjs`가 독립 정적 서버에서 320·390·768·1440·1920·2560px를 열었다. 각 폭의 문서 `scrollWidth`는 뷰포트 폭과 같고, 내비는 한 줄, 버튼은 최소 44px이었다.
- 320px에서 200% 텍스트 확대 후 문서 폭은 320px, 서술형 h1은 80px으로 실제 확대됐다. 장식 표본은 `aria-hidden="true"`로 남았다.
- `prefers-reduced-motion: reduce`에서 `scroll-behavior`는 `auto`였다. 키보드 Space로 제목 버튼을 선택하면 `aria-pressed`와 상태 문구가 마루 부리로 바뀌었다.
- 세 range는 24px·0.05em·2.2를 표본과 캡션에 적용했고, 리셋은 17px·0em·1.8을 둘 다 복원했다.
- 지연 이미지 두 장은 scrollIntoView 뒤 `decode()`를 통과했고, `metaltype.webp` 응답은 HTTP 200·naturalWidth 900이었다. `font-foundry-1440-first.png`, `font-foundry-390-first.png`, 전체 캡처와 JSON 보고서는 같은 outputs 폴더에 있다.
## Round 4 판단표 · 2026-09-12
| 축 | 참고 범위와 현재 질문 | 채택과 기각 조건 | 구현 |
| --- | --- | --- | --- |
| 구도 | Arnheim·Hofmann의 균형·대비·구도 관점을 참고해 소개와 표본의 관계, 5:7 대안, 작은 폭에서의 순서 유지를 살핀다. | 소개와 표본의 관계가 읽히고 작은 폭에서 순서가 유지되면 채택한다. CTA가 밀리면 기각한다. | `--container-wide`, `--pad-wide` |
| 타입 | Ruder의 구조·리듬·비례와 Wallace의 개인·성능·선호 분리 범위를 참고해 같은 문장·22px·1.8·동일 폭 두 칼럼과 가족 선택을 살핀다. | 조절과 reset이 깨지지 않을 때만 채택한다. 읽기 성능의 보편 효과는 주장하지 않는다. | `--font-display`, `--font-body` |
| 색 | Albers의 색 맥락 관점을 참고해 작은 accent와 큰 stage의 관계를 비교한다. | 역할 경계와 대비가 유지되고 상태가 묻히지 않을 때 채택한다. | `--stage-ink`, `--accent` |
## Round 4 실증 기록 · 2026-09-12
- **방향과 원리**: 한성활자연구소의 종이색·적갈색·금속 활자 이미지는 유지하고, 첫 화면을 5:7 종이 소개면과 적갈색 표본면으로 재배치했다. 표본면의 큰 흰 글자, 기준선, 선택 상태는 같은 시선 안에서 글꼴 전환을 읽게 한다.
- **비교 조건**: Ruder의 구조·리듬·비례를 현재 화면의 검증 질문으로만 번역했다. 비교 두 칼럼은 같은 원문, 동일 폭, 기본 22px(`1.375rem`)와 `1.8` 행간을 공유하며 글꼴 가족만 다르다. 서체마다 글자가 보이는 크기는 다를 수 있다는 안내를 함께 둔다.
- **채택과 측정 한계**: 조판 시험대는 실제 웹폰트 선택을 문장에 적용하고 네 제어값을 리셋한다. 화면 비교는 형태·폭·줄바꿈의 관찰을 돕지만, 읽기 속도·이해도 향상은 사용자 과업 측정 전까지 주장하지 않는다. 역사처럼 보일 수 있는 견본 문단은 견본글임을 명시했다.
- **동일 브리프 보존**: 가상 기획·예시 가격·라이선스 안내, 네 개의 내비와 앵커, 기존 CTA, 금속 이미지와 생성 이미지 표기, 버튼의 `aria-pressed`·상태 문구, 기존 조판 range와 reset 기능을 보존한다.
### Round 4 최종 판정
2026-09-12 원본 담당자 판정: 수용. 소개와 표본의 면 구분, 표본과 제어의 군집, 같은 조건의 문장 비교를 실제 데스크톱·모바일 캡처에서 확인했다. 작은 폭의 확대에서 발견한 내비·가격·제목의 넘침과 앵커 가림을 수정했다.
`outputs/design-expertise-20260912/round4/browser-proof/root-final-v3/feature-check-results.json`: 수정본 320·390·1440·1920px 4개 행 통과, 실패 0. 글꼴 실제 로딩, 키보드 선택·range·초기화, 앵커, 감소 모션, 이미지 decode, 문서 가로 넘침을 검사했다. 확대 검사는 루트 글자 크기 200% 및 자간 0.12em·행간 2의 조합이며, 브라우저 확대나 WCAG 전체 적합성 인증을 대신하지 않는다. 표본면의 설명·상태·선택/비선택 버튼 텍스트 대비는 6.07:1이었다. 사용자 읽기 속도·이해도·전환은 미측정이다.
## 2026-09-12 전체 쇼케이스 점검
- 기존 글꼴 선택·range·reset, 22px·1.8·동일 폭 비교 계약과 캡션·앵커 구도를 유지했다. 로컬 MaruBuri 경로는 `fonts.css?v=20260912all`로 명시했다.
- `verify-final` 18개 묶음은 16개가 즉시 통과했다. 전체 묶음의 invariant·visual 검사는 Gaon·Dure를 대상으로 하며, 나머지 2개는 수정 뒤 release 독립 재실행에서 ALL PASS·ALL MATCH였다.
- root는 28개 모바일, 21개 데스크톱, 7개 하위 경로와 30개 앱 상태를 직접 검토해 수용했다. stress 112건은 hard failure 0이며, 가로 넘침 4건은 targeted fix 뒤 390px 재측정에서 0이다.
- 이 기록은 렌더·계약 범위의 현재 결과다. 모든 폭·키보드 경로·인쇄 등 개별 기능을 완전 재실행했다는 뜻이 아니며, 참여자 과업 효과도 미측정이다.

View file

@ -10,9 +10,9 @@
<meta property="og:type" content="website" />
<meta name="theme-color" content="#F5F1E8" />
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/webfontworld/naver/MaruBuri.css" />
<link rel="stylesheet" href="./styles/fonts.css?v=20260912all" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css" />
<link rel="stylesheet" href="./styles/main.css?v=3" />
<link rel="stylesheet" href="./styles/main.css?v=20260912all" />
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23F5F1E8'/%3E%3Crect x='8' y='8' width='16' height='16' fill='%23A63D2F'/%3E%3C/svg%3E" />
</head>
<body>
@ -37,7 +37,7 @@
<h1 id="hero-h">읽는 리듬을<br />만드는 활자.</h1>
</div>
<div class="hero-note">
<p>글자는 모양만으로 읽히지 않습니다. 문장 안에서 어떤 폭으로 서고, 얼마나 가까이 호흡하는지가 읽는 시간을 바꿉니다.</p>
<p>글자는 모양만으로 읽히지 않습니다. 문장 안에서 어떤 폭으로 서고, 얼마나 가까이 호흡하는지 직접 관찰해 보세요.</p>
<a class="text-link" href="#bench">조판해 보기 <span aria-hidden="true">↓</span></a>
</div>
</div>
@ -59,20 +59,21 @@
<div class="wrap">
<div class="section-head">
<p class="eyebrow">두 가지 읽기 방식</p>
<h2 id="typefaces-h">같은 문장이<br />다른 속도로 읽힌다.</h2>
<h2 id="typefaces-h">같은 문장, 서로 다른 결.</h2>
</div>
<p class="comparison-note">크기·행간·폭을 맞췄습니다. 서체마다 글자가 보이는 크기는 다를 수 있습니다.</p>
<div class="comparison">
<article class="typeface">
<header class="typeface-head">
<h3>본문</h3>
<p class="muted">Pretendard · 작고 긴 문장에 필요한 명료한 골격</p>
<h3>Pretendard</h3>
<p class="muted">본문용 산세리프</p>
</header>
<p class="specimen specimen-body">아침 안개가 걷히기 전에 도랑물 소리가 먼저 들렸다. 글자는 종이 위에서 소리를 내지 않지만, 잘 짜인 활자는 소리 내어 읽게 만든다.</p>
</article>
<article class="typeface typeface-title">
<header class="typeface-head">
<h3>제목</h3>
<p class="muted">마루 부리 · 표제에서 멈춤과 여백을 만드는 결</p>
<h3>MaruBuri</h3>
<p class="muted">명조 계열 세리프</p>
</header>
<p class="specimen specimen-display">아침 안개가 걷히기 전에 도랑물 소리가 먼저 들렸다. 글자는 종이 위에서 소리를 내지 않지만, 잘 짜인 활자는 소리 내어 읽게 만든다.</p>
</article>
@ -86,10 +87,11 @@
<p class="eyebrow">조판 시험대</p>
<h2 id="bench-h">읽는 호흡을<br />직접 맞춰 보세요.</h2>
</div>
<p class="muted bench-lead">크기는 문장의 밀도를, 자간은 낱글자 사이의 공기를, 행간은 다음 줄로 넘어가는 속도를 바꿉니다. 하나씩 움직여 같은 문장이 어떻게 달라지는지 비교해 보세요.</p>
<p class="muted bench-lead">크기·자간·행간을 바꾸며 문장 폭, 줄바꿈, 글자 형태의 차이가 어떻게 보이는지 비교해 보세요.</p>
</div>
<div class="bench">
<form class="bench-controls" aria-label="조판 설정">
<div class="control"><label for="ctl-font">글꼴</label><select id="ctl-font" name="font"><option value="display">마루부리</option><option value="body">Pretendard</option></select></div>
<div class="control"><div class="control-head"><label for="ctl-size">글자 크기</label><output id="out-size" for="ctl-size">17px</output></div><input type="range" id="ctl-size" name="size" min="13" max="26" step="1" value="17" /></div>
<div class="control"><div class="control-head"><label for="ctl-tracking">자간</label><output id="out-tracking" for="ctl-tracking">0em</output></div><input type="range" id="ctl-tracking" name="tracking" min="-5" max="10" step="1" value="0" /></div>
<div class="control"><div class="control-head"><label for="ctl-leading">행간</label><output id="out-leading" for="ctl-leading">1.8</output></div><input type="range" id="ctl-leading" name="leading" min="1.4" max="2.4" step="0.1" value="1.8" /></div>
@ -97,6 +99,7 @@
</form>
<div class="bench-reading">
<p id="bench-caption" class="bench-caption">마루 부리 · 17px · 자간 0em · 행간 1.8</p>
<p class="bench-disclaimer">아래 문단은 역사 사실을 설명하는 글이 아닌 조판 견본글입니다.</p>
<div class="bench-sample" id="bench-sample" aria-live="polite">활자의 역사는 글자를 찍는 기계의 역사가 아니라, 글자를 읽는 눈의 역사였다. 활자소의 식자공들은 하루에 만 글자를 짜면서 어느 글꼴이 눈을 덜 아프게 하는지 알고 있었다. 그 지식은 문서로 남지 않았고 손끝으로만 전해졌다. 이 미리보기는 그 판단을 눈으로 비교할 수 있게 옮긴 것입니다. 자간을 0에서 5로 바꾸면 문단의 호흡과 줄 길이가 어떻게 달라지는지 확인할 수 있습니다.</div>
</div>
</div>
@ -147,20 +150,22 @@
typeStatus.textContent = typeMessages[kind];
}));
const sample = document.getElementById("bench-sample");
const ctl = { size: document.getElementById("ctl-size"), tracking: document.getElementById("ctl-tracking"), leading: document.getElementById("ctl-leading") };
const ctl = { font: document.getElementById("ctl-font"), size: document.getElementById("ctl-size"), tracking: document.getElementById("ctl-tracking"), leading: document.getElementById("ctl-leading") };
const out = { size: document.getElementById("out-size"), tracking: document.getElementById("out-tracking"), leading: document.getElementById("out-leading") };
const defaults = { size: 17, tracking: 0, leading: 1.8 };
const defaults = { font: "display", size: 17, tracking: 0, leading: 1.8 };
const fontNames = { display: "마루 부리", body: "Pretendard" };
function apply() {
sample.style.fontFamily = ctl.font.value === "display" ? "var(--font-display)" : "var(--font-body)";
sample.style.fontSize = ctl.size.value + "px";
sample.style.letterSpacing = ctl.tracking.value / 100 + "em";
sample.style.lineHeight = ctl.leading.value;
out.size.textContent = ctl.size.value + "px";
out.tracking.textContent = ctl.tracking.value / 100 + "em";
out.leading.textContent = Number(ctl.leading.value).toFixed(1);
document.getElementById("bench-caption").textContent = `마루 부리 · ${ctl.size.value}px · 자간 ${ctl.tracking.value / 100}em · 행간 ${Number(ctl.leading.value).toFixed(1)}`;
document.getElementById("bench-caption").textContent = `${fontNames[ctl.font.value]} · ${ctl.size.value}px · 자간 ${ctl.tracking.value / 100}em · 행간 ${Number(ctl.leading.value).toFixed(1)}`;
}
Object.values(ctl).forEach((input) => input.addEventListener("input", apply));
document.getElementById("ctl-reset").addEventListener("click", () => { ctl.size.value = defaults.size; ctl.tracking.value = defaults.tracking; ctl.leading.value = defaults.leading; apply(); });
Object.values(ctl).forEach((input) => { input.addEventListener("input", apply); input.addEventListener("change", apply); });
document.getElementById("ctl-reset").addEventListener("click", () => { ctl.font.value = defaults.font; ctl.size.value = defaults.size; ctl.tracking.value = defaults.tracking; ctl.leading.value = defaults.leading; apply(); });
apply();
</script>
</body>

View file

@ -0,0 +1,15 @@
@font-face {
font-family: "MaruBuri";
src: url("../assets/fonts/MaruBuri-Regular.woff2") format("woff2");
font-style: normal;
font-weight: 400;
font-display: swap;
}
@font-face {
font-family: "MaruBuri";
src: url("../assets/fonts/MaruBuri-SemiBold.woff2") format("woff2");
font-style: normal;
font-weight: 600;
font-display: swap;
}

View file

@ -1,4 +1,4 @@
@import url("./tokens.css?v=3");
@import url("./tokens.css?v=20260912all");
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
@ -19,36 +19,41 @@ a { color: inherit; }
.site-head { position: sticky; z-index: 10; top: 0; border-bottom: 1px solid var(--line); background: color-mix(in srgb, var(--surface) 94%, transparent); backdrop-filter: blur(6px); }
.head-inner { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-4); min-block-size: 4.25rem; }
.wordmark { font-family: var(--font-display); font-size: var(--step-1); font-weight: 600; text-decoration: none; }
.nav { display: flex; gap: clamp(var(--space-3), 2vw, var(--space-5)); font-size: var(--step--1); }
.nav { display: flex; flex-wrap: wrap; gap: clamp(var(--space-3), 2vw, var(--space-5)); font-size: var(--step--1); }
.nav a { padding-block: var(--space-2); color: var(--ink-muted); text-decoration: none; }
.nav a:hover { color: var(--accent); }
.hero { padding-block: clamp(var(--space-6), 7vw, var(--space-8)) var(--space-7); }
.hero-rail { width: min(100% - 2 * var(--pad-wide), var(--container-wide)); margin-inline: auto; }
.hero-intro { display: grid; grid-template-columns: minmax(0, 7fr) minmax(17rem, 3fr); gap: var(--space-6) clamp(var(--space-5), 7vw, var(--space-8)); align-items: end; }
.hero h1 { margin-top: var(--space-3); font-size: clamp(2.5rem, 1.3rem + 4.2vw, 5rem); max-inline-size: 10ch; }
.hero-note { max-inline-size: 27rem; padding-bottom: var(--space-2); }
.hero { padding-block: clamp(var(--space-5), 5vw, var(--space-7)) var(--space-7); }
.hero-rail { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: clamp(var(--space-5), 5vw, var(--space-8)); width: min(100% - 2 * var(--pad-wide), var(--container-wide)); margin-inline: auto; align-items: stretch; }
.hero-rail > *, .hero-intro > * { min-inline-size: 0; }
.hero-intro { display: flex; flex-direction: column; justify-content: space-between; gap: var(--space-6); min-block-size: clamp(32rem, 52vw, 44rem); padding: clamp(var(--space-5), 4vw, var(--space-7)); border: 1px solid var(--line); background: var(--surface-raised); }
.hero h1 { margin-top: var(--space-3); font-size: clamp(2.5rem, 1.6rem + 3vw, 3.75rem); max-inline-size: 10ch; }
.hero-note { max-inline-size: 29rem; }
.text-link { display: inline-flex; gap: var(--space-2); margin-top: var(--space-4); color: var(--accent); font-weight: 600; text-decoration-thickness: 1px; text-underline-offset: 0.25em; }
.specimen-stage { margin-top: clamp(var(--space-6), 8vw, var(--space-8)); color: var(--accent); }
.stage-label { color: var(--ink-muted); }
.hero-specimen { min-inline-size: 0; margin-top: var(--space-4); font-family: var(--font-body); font-size: clamp(6rem, 17.5vw, 16.25rem); font-weight: 650; letter-spacing: -0.09em; line-height: 0.82; text-wrap: nowrap; }
.specimen-stage { display: flex; flex-direction: column; justify-content: space-between; min-inline-size: 0; min-block-size: clamp(32rem, 52vw, 44rem); padding: clamp(var(--space-5), 4vw, var(--space-7)); background: var(--accent); color: var(--stage-ink); }
.stage-label { color: var(--stage-ink); }
.hero-specimen { min-inline-size: 0; margin-block: var(--space-5); font-family: var(--font-body); font-size: clamp(6rem, 11vw, 11.25rem); font-weight: 650; letter-spacing: -0.09em; line-height: 0.82; overflow-wrap: anywhere; white-space: normal; }
.specimen-stage[data-kind="title"] .hero-specimen { font-family: var(--font-display); font-weight: 600; letter-spacing: -0.11em; }
.baseline { block-size: 1px; margin-top: var(--space-3); background: var(--accent); }
.baseline { block-size: 1px; margin-top: var(--space-3); background: color-mix(in srgb, var(--stage-ink) 75%, transparent); }
.stage-footer { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); padding-top: var(--space-3); }
.type-switch { display: flex; gap: var(--space-2); }
.type-switch { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.type-choice, .btn-reset { min-block-size: 2.75rem; border: 1px solid var(--line); border-radius: var(--radius); background: transparent; color: var(--ink); cursor: pointer; font: inherit; padding-inline: 0.9rem; }
.type-choice[aria-pressed="true"] { border-color: var(--accent); background: var(--accent); color: var(--accent-ink); }
.type-status { color: var(--ink-muted); font-size: var(--step--1); text-align: end; }
.type-choice { border-color: color-mix(in srgb, var(--stage-ink) 72%, transparent); color: var(--stage-ink); }
.type-choice[aria-pressed="true"] { border-color: var(--stage-ink); background: var(--stage-ink); color: var(--accent); }
.type-choice:focus-visible { outline-color: var(--stage-ink); }
.type-status { color: var(--stage-ink); font-size: var(--step--1); text-align: end; }
.section { padding-block: var(--space-8); }
.section[id] { scroll-margin-block-start: 5rem; }
.band { border-block: 1px solid var(--line); background: var(--surface-raised); }
.section-head { display: grid; grid-template-columns: minmax(0, 4fr) minmax(0, 8fr); gap: var(--space-5); align-items: baseline; }
.comparison { margin-top: var(--space-7); border-top: 1px solid var(--line); }
.typeface { display: grid; grid-template-columns: minmax(10rem, 3fr) minmax(0, 7fr); gap: var(--space-5) var(--space-7); padding-block: var(--space-6); border-bottom: 1px solid var(--line); }
.comparison-note { max-inline-size: 42rem; margin-top: var(--space-5); color: var(--ink-muted); }
.comparison { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-6); margin-top: var(--space-6); border-top: 1px solid var(--line); }
.typeface { min-inline-size: 0; padding-block: var(--space-6); border-bottom: 1px solid var(--line); }
.typeface-head .muted { margin-top: var(--space-2); font-size: var(--step--1); }
.specimen { max-inline-size: 42rem; }
.specimen-body { font-family: var(--font-body); font-size: 1.15rem; line-height: 1.9; }
.specimen-display { font-family: var(--font-display); font-size: clamp(1.7rem, 1.2rem + 2.3vw, 3.25rem); line-height: 1.48; }
.specimen { margin-top: var(--space-5); inline-size: 100%; font-size: 1.375rem; line-height: 1.8; }
.specimen-body { font-family: var(--font-body); }
.specimen-display { font-family: var(--font-display); }
.bench-heading { display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 4fr); gap: var(--space-6); align-items: end; }
.bench-lead { max-inline-size: 28rem; }
@ -58,10 +63,13 @@ a { color: inherit; }
.control-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); }
.control label, .control output { color: var(--ink-muted); font-size: var(--step--1); }
.control output, .order-price { font-variant-numeric: tabular-nums; }
input[type="range"] { inline-size: 100%; accent-color: var(--accent); }
input[type="range"], select { inline-size: 100%; accent-color: var(--accent); }
select { min-block-size: 2.75rem; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); color: var(--ink); font: inherit; padding-inline: var(--space-3); }
.btn-reset { justify-self: start; margin-top: var(--space-2); }
.btn-reset:hover, .type-choice:hover { border-color: var(--accent); }
.btn-reset:hover { border-color: var(--accent); }
.type-choice:hover { border-color: var(--stage-ink); }
.bench-caption { margin-bottom: var(--space-3); }
.bench-disclaimer { margin-bottom: var(--space-3); color: var(--ink-muted); font-size: var(--step--1); }
.bench-sample { max-inline-size: 42rem; border: 1px solid var(--line); background: var(--surface); padding: clamp(var(--space-4), 3vw, var(--space-6)); font-family: var(--font-display); font-size: 1.0625rem; line-height: 1.8; }
.case-grid { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: var(--space-6) var(--space-7); align-items: start; }
@ -85,23 +93,31 @@ input[type="range"] { inline-size: 100%; accent-color: var(--accent); }
.foot-inner { display: flex; flex-wrap: wrap; justify-content: space-between; gap: var(--space-3) var(--space-5); }
@media (max-width: 900px) {
.hero-intro, .section-head, .bench-heading, .bench, .case-grid, .order-grid { grid-template-columns: 1fr; }
.hero-rail, .section-head, .comparison, .bench-heading, .bench, .case-grid, .order-grid { grid-template-columns: 1fr; }
.hero-intro, .specimen-stage { min-block-size: 0; }
.hero-intro, .bench-heading { gap: var(--space-4); }
.hero-note { max-inline-size: 34rem; }
.typeface { grid-template-columns: 1fr; gap: var(--space-3); }
.bench { gap: var(--space-5); }
.case-photo { max-inline-size: 30rem; }
}
@media (max-width: 560px) {
.head-inner { display: grid; gap: 0; min-block-size: 0; padding-block: var(--space-3); }
.nav { justify-content: space-between; gap: var(--space-2); max-inline-size: 100%; margin-top: var(--space-1); overflow-x: auto; }
.nav { justify-content: space-between; gap: var(--space-2); max-inline-size: 100%; margin-top: var(--space-1); }
.nav a { flex: 0 0 auto; padding-block: var(--space-1); }
.hero { padding-block: var(--space-6); }
.hero { padding-block: var(--space-5); }
.hero-rail { gap: var(--space-4); }
.hero-intro, .specimen-stage { padding: var(--space-4); }
.hero-intro :where(p, h1), .stage-label, .type-status { overflow-wrap: anywhere; }
.hero h1 { font-size: clamp(2.5rem, 11vw, 4rem); overflow-wrap: anywhere; word-break: normal; }
.hero-specimen { font-size: min(30vw, 9.5rem); letter-spacing: -0.12em; }
.bench-heading > * { min-inline-size: 0; }
.bench-heading h2 { overflow-wrap: anywhere; }
.order-row > * { min-inline-size: 0; max-inline-size: 100%; }
.order-price { white-space: normal; overflow-wrap: anywhere; }
.stage-footer, .order-row { align-items: flex-start; flex-direction: column; }
.type-status { text-align: start; }
.section { padding-block: var(--space-7); }
.section[id] { scroll-margin-block-start: calc(7rem + 1rem); }
}
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }

View file

@ -16,6 +16,7 @@
--line: #d9d1bf;
--accent: #a63d2f;
--accent-ink: #f5f1e8;
--stage-ink: #fffaf1;
--accent-hover: #8a3225;
--space-1: 0.25rem;
--space-2: 0.5rem;

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,13 @@
# MaruBuri source record
Downloaded 2026-09-12 from the official NAVER Hangeul static host. The files are retained unchanged: no conversion or subsetting was performed.
| File | Official URL | HTTP content type | Size | SHA-256 |
| --- | --- | --- | --- | --- |
| `MaruBuri-Regular.woff2` | https://hangeul.pstatic.net/hangeul_static/webfont/MaruBuri/MaruBuri-Regular.woff2 | `font/woff2` | 433776 bytes | `4CF1341CF2F23FB3E263712DFDE1D8F25EEDCC328B696A2E5A2C8ADD55E5C17B` |
| `MaruBuri-SemiBold.woff2` | https://hangeul.pstatic.net/hangeul_static/webfont/MaruBuri/MaruBuri-SemiBold.woff2 | `font/woff2` | 466108 bytes | `7AEF4862FA3219B0A7DEB6746E27116A38C4069F4424EB51EDF103B29795F5C2` |
- Official stylesheet checked: https://hangeul.pstatic.net/hangeul_static/css/maru-buri.css
- Official NAVER license page saved verbatim as `LICENSE.html`: https://help.naver.com/service/11029/contents/18088?lang=ko&osType=PC
- `LICENSE.html` SHA-256: `8538F7921744D5E040F603BC301465EFBD038F1054D0754B7508DE3286452EC3`
- The saved official page includes the NAVER copyright notice and the SIL Open Font License 1.1 text for MaruBuri.

View file

@ -41,3 +41,10 @@
- `prefers-reduced-motion`과 효과 제거 상태에서도 콘텐츠가 같은 순서로 읽히는지 점검한다.
- 200% 텍스트 확대에서 본문·CTA가 사라지지 않는지 점검한다.
- 실행 결과와 화면 캡처는 `outputs/design-expertise-20260912/form/`에 남긴다.
## 2026-09-12 전체 쇼케이스 점검
- 기존 흑백 사진·도면 구도, 앵커와 긴 모바일 라벨의 줄바꿈 규칙을 보존했다. 로컬 MaruBuri 파일을 함께 제공하되 현재 Pretendard 단일 패밀리 본문 계약은 바꾸지 않았다.
- `verify-final` 18개 묶음은 16개가 즉시 통과했다. 전체 묶음의 invariant·visual 검사는 Gaon·Dure를 대상으로 하며, 나머지 2개는 수정 뒤 release 독립 재실행에서 ALL PASS·ALL MATCH였다.
- root는 28개 모바일, 21개 데스크톱, 7개 하위 경로와 30개 앱 상태를 직접 검토해 수용했다. stress 112건은 hard failure 0이며, 가로 넘침 4건은 targeted fix 뒤 390px 재측정에서 0이다.
- 이 기록은 렌더·계약 범위의 현재 결과다. 모든 폭·키보드 경로·인쇄 등 개별 기능을 완전 재실행했다는 뜻이 아니며, 참여자 과업 효과도 미측정이다.

View file

@ -10,7 +10,8 @@
<meta property="og:type" content="website" />
<meta name="theme-color" content="#fdfdfb" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css" />
<link rel="stylesheet" href="./styles/main.css?v=3" />
<link rel="stylesheet" href="./styles/fonts.css?v=20260912all" />
<link rel="stylesheet" href="./styles/main.css?v=20260912all" />
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect x='6' y='6' width='20' height='20' fill='none' stroke='%23171b19' stroke-width='2'/%3E%3Crect x='11' y='11' width='10' height='10' fill='%23171b19'/%3E%3C/svg%3E" />
</head>
<body>

View file

@ -0,0 +1,15 @@
@font-face {
font-family: "MaruBuri";
src: url("../assets/fonts/MaruBuri-Regular.woff2") format("woff2");
font-style: normal;
font-weight: 400;
font-display: swap;
}
@font-face {
font-family: "MaruBuri";
src: url("../assets/fonts/MaruBuri-SemiBold.woff2") format("woff2");
font-style: normal;
font-weight: 600;
font-display: swap;
}

View file

@ -1,4 +1,4 @@
@import url("./tokens.css?v=3");
@import url("./tokens.css?v=20260912all");
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }

View file

@ -0,0 +1,24 @@
# 가온 학적부 디자인 기록
## 브리프
- 무엇을: 학원 강사가 수업·출석·과제·상담을 기록으로 확인하는 학적부형 관리 화면.
- 누구에게: 행과 상태를 빠르게 대조하고 다음 업무를 정해야 하는 강사.
- 제약: 예시 데이터와 기존 계산·상태·모바일 내비·대화상자·인쇄 흐름을 보존한다.
## 판단 기준
| 축 | 질문 | 적용 |
| --- | --- | --- |
| Warde · 정보 전달 | 다음 확인 대상이 수치보다 먼저 읽히는가 | 공통 정렬선과 가는 구분선의 요약 기록 밴드, 15px 표 본문 |
| Ruder · 비교 변수 | 같은 종류의 값이 같은 열과 숫자 폭에서 비교되는가 | 표 숫자에 tabular figures, 수치 열 오른쪽 정렬 |
| Albers · 상태 맥락 | 상태색이 주변의 중립 기록면에서 구분되는가 | 상태는 텍스트와 함께 유지하고 배경색은 보조로 사용 |
Warde, Ruder, Albers는 정보 전달·비교 변수·상태 맥락을 검토하는 참고 질문이며 인과나 성능 법칙이 아니다.
## 역할 토큰과 검증
- `--record-radius`, `--record-rule`, `--record-body-size`, `--record-meta-size`가 기록면·구분선·본문·보조정보 역할을 고정한다.
- `verify-final` 18개 묶음은 16개가 즉시 통과했다. 전체 묶음의 invariant·visual 검사는 Gaon·Dure를 대상으로 하며, 나머지 2개는 수정 뒤 release 독립 재실행에서 ALL PASS·ALL MATCH였다.
- root는 28개 모바일, 21개 데스크톱, 7개 하위 경로와 30개 앱 상태를 직접 검토해 수용했다. stress 112건은 hard failure 0이며, 가로 넘침 4건은 targeted fix 뒤 390px 재측정에서 0이다.
- 이 기록은 렌더·계약 범위의 현재 결과다. 모든 폭·키보드 경로·인쇄 등 개별 기능을 완전 재실행했다는 뜻이 아니며, 참여자 과업 효과도 미측정이다.

View file

@ -9,15 +9,15 @@
<link rel="manifest" href="./manifest.webmanifest" />
<link rel="apple-touch-icon" href="./icon-192.png" />
<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/gaon-lms/" />
<meta property="og:image" content="https://designpaca.chanpaca.net/work/gaon-lms/og.png" />
<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/gaon-lms/og.png" />
<meta
name="description"
content="학원 강사를 위한 학습관리 대시보드. 출석부, 수강생 기록, 과제 채점, 학사 일정까지 여덟 화면. 데모 데이터로 실제로 작동합니다."
content="학원 강사를 위한 학습 관리 화면입니다. 출석, 수강생 기록, 과제 채점, 학사 일정을 한곳에서 살펴볼 수 있습니다."
/>
<meta property="og:title" content="가온 학적부 · 학습관리 대시보드" />
<meta property="og:description" content="출석부에서 출발한 학습관리 대시보드." />
@ -35,7 +35,7 @@
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Noto+Serif+KR:wght@500;600&display=swap"
/>
<link rel="stylesheet" href="./styles/main.css?v=24" />
<link rel="stylesheet" href="./styles/main.css?v=20260912all" />
<link
rel="icon"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%23fafafa'/%3E%3Crect x='6.5' y='6.5' width='19' height='19' rx='4' fill='%23059669'/%3E%3Crect x='10' y='12' width='12' height='1.8' rx='0.9' fill='%23ffffff'/%3E%3Crect x='10' y='15.5' width='8' height='1.8' rx='0.9' fill='%23ffffff' opacity='0.85'/%3E%3Crect x='10' y='19' width='10' height='1.8' rx='0.9' fill='%23ffffff' opacity='0.7'/%3E%3C/svg%3E"
@ -141,6 +141,7 @@
<div>
<h2 class="v-h" id="dash-h">이번 주, 열두 명 중 열 명</h2>
<p class="view-sub">3월 9일 월요일 오전 기준. 명단 12명 중 재학 10명, 주간 출석은 지난 주(3/2–3/6) 결산입니다.</p>
<p class="panel-note muted" role="note">표시된 수강생·수업·상담 기록은 예시입니다. 이 화면에서 바꾼 내용은 새로고침하면 초기화됩니다.</p>
</div>
</header>
@ -217,6 +218,7 @@
<span class="key"><i class="k-abs"></i>결석</span>
</figcaption>
</figure>
<div class="table-wrap" tabindex="0" aria-label="요일별 출석 결산 표 가로 스크롤">
<table class="wk-sum">
<caption class="sr-only">요일별 출석 결산</caption>
<thead><tr><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" class="num">출석률</th></tr></thead>
@ -229,6 +231,7 @@
</tbody>
<tfoot><tr><td>합계</td><td class="num">46</td><td class="num">2</td><td class="num">2</td><td class="num"><span class="hl">92%</span></td></tr></tfoot>
</table>
</div>
</section>
<!-- 마감 임박 -->
@ -550,7 +553,7 @@
</thead>
<tbody id="file-body"><!-- JS --></tbody>
</table>
<p class="panel-note muted">다운로드 링크를 만들지 않는다 — 이 데모에는 파일이 없다. 배부 상태만 기록한다.</p>
<p class="panel-note muted">배부 여부와 열람 현황을 확인하는 목록입니다. 원본 파일은 이 화면에서 제공하지 않습니다.</p>
</div>
</section>
@ -798,7 +801,7 @@
<script src="calc.js?v=2"></script>
<script>
/*
앱 상태는 전부 이 안에 있다. 서버도 localStorage 도 없다 — 데모 원칙.
화면 상태는 새로고침하면 초기화한다. 서버와 localStorage에는 저장하지 않는다.
뷰 전환 · 검색/필터 · 출석 순환 · 배부 상태 · 리포트 계산까지
네이티브 기능 위에 얹은 얇은 JS.
*/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Before After
Before After

View file

@ -76,6 +76,7 @@ h3 { font-size: var(--step-0); font-weight: 500; }
display: grid;
grid-template-columns: 244px minmax(0, 1fr);
min-height: 100dvh;
min-width: 0;
background: var(--background);
}
@ -149,7 +150,7 @@ h3 { font-size: var(--step-0); font-weight: 500; }
.topbar .brand { display: none; } /* 브랜드는 사이드바에 있다 — 모바일에서만 탑바가 이어받는다 */
.crumb { display: flex; align-items: center; gap: var(--space-2); font-size: var(--step-0); color: var(--muted-foreground); }
.crumb strong { color: var(--foreground); font-weight: 600; }
.topbar-right { display: flex; align-items: center; gap: var(--space-3); }
.topbar-right { display: flex; min-width: 0; align-items: center; gap: var(--space-3); }
.term-badge {
display: inline-flex; align-items: center;
font-size: var(--step-1); font-weight: 500;
@ -170,11 +171,11 @@ h3 { font-size: var(--step-0); font-weight: 500; }
/* ── 본문 ── */
.main { min-width: 0; display: flex; flex-direction: column; }
.view { display: none; }
.view { display: none; min-width: 0; }
.view.is-on { display: block; }
.view:focus { outline: none; }
.view-sub { color: var(--muted-foreground); font-size: var(--step-0); margin-top: 4px; max-width: 46em; }
.view-sub { color: var(--muted-foreground); font-size: var(--record-meta-size); margin-top: 4px; max-width: 46em; }
.view-head {
display: flex; justify-content: space-between; align-items: flex-start;
gap: var(--space-4);
@ -193,23 +194,23 @@ h3 { font-size: var(--step-0); font-weight: 500; }
.panel {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
border-radius: var(--record-radius);
box-shadow: none;
padding: var(--space-5);
min-width: 0;
}
.panel h2 { margin-bottom: var(--space-4); font-size: var(--step-2); }
.panel-note { font-size: var(--step-1); color: var(--faint); margin-top: var(--space-3); }
.panel-note { font-size: var(--record-meta-size); color: var(--faint); margin-top: var(--space-3); }
/* 통계 카드 */
.headline { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-4); }
.headline { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0; background: var(--card); border: 1px solid var(--record-rule); border-radius: var(--record-radius); box-shadow: none; font-variant-numeric: tabular-nums; }
.headline > div {
background: var(--card); border: 1px solid var(--border); border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm); padding: var(--space-4) var(--space-5);
min-width: 0; padding: var(--space-4) var(--space-5) var(--space-5); background: transparent; border: 0; border-right: 1px solid var(--record-rule); border-radius: 0; box-shadow: none;
}
.headline dt { font-size: var(--step-0); color: var(--muted-foreground); font-weight: 500; }
.headline > div:last-child { border-right: 0; }
.headline dt { font-size: var(--record-meta-size); color: var(--muted-foreground); font-weight: 500; }
.headline dd { font-size: var(--step-3); font-weight: 600; letter-spacing: -0.03em; line-height: 1.2; font-variant-numeric: tabular-nums; margin-top: 4px; }
.headline .stat-sub { font-size: var(--step--1); color: var(--faint); margin: 6px 0 0; margin-inline-start: 0; font-variant-numeric: tabular-nums; }
.headline .stat-sub { font-size: var(--record-meta-size); color: var(--faint); margin: 6px 0 0; margin-inline-start: 0; font-variant-numeric: tabular-nums; }
.dash-grid {
display: grid;
@ -221,15 +222,19 @@ h3 { font-size: var(--step-0); font-weight: 500; }
.week-panel { grid-row: span 2; }
/* ── 표 ── */
.table-wrap { overflow-x: auto; overflow-y: hidden; }
table { width: 100%; border-collapse: collapse; }
.table-wrap { max-width: 100%; overflow-x: auto; overflow-y: hidden; overscroll-behavior-inline: contain; scrollbar-gutter: stable both-edges; }
table { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
.table-wrap > table { min-width: 600px; }
#stu-table, #att-table, #as-table { min-width: 760px; }
#report-table { min-width: 680px; }
table th, table td { overflow-wrap: normal; word-break: keep-all; }
thead th {
text-align: left; font-weight: 500; font-size: var(--step-1);
text-align: left; font-weight: 600; font-size: var(--record-meta-size);
color: var(--muted-foreground); padding: 0 var(--space-3) var(--space-3);
border-bottom: 1px solid var(--border);
height: 36px; white-space: nowrap;
}
tbody td, tbody th { padding: var(--space-3); border-bottom: 1px solid var(--border); vertical-align: middle; text-align: left; }
tbody td, tbody th { padding: var(--space-3); border-bottom: 1px solid var(--border); vertical-align: middle; text-align: left; font-size: var(--record-body-size); line-height: 1.55; }
tbody th { font-weight: 500; }
tbody tr:last-child td, tbody tr:last-child th { border-bottom: 0; }
tbody tr { transition: background-color 80ms ease; }
@ -239,7 +244,7 @@ tbody tr:hover { background: var(--muted); }
.row-open {
appearance: none; border: 0; background: none; padding: 0;
font: inherit; font-weight: 500; color: var(--foreground);
cursor: pointer; text-align: left;
min-height: 24px; cursor: pointer; text-align: left;
transition: color 80ms ease;
}
.row-open:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; }
@ -285,8 +290,8 @@ tbody tr:hover { background: var(--muted); }
.wk-sum thead th { padding: 8px var(--space-2); }
.wk-sum tbody td { padding: 7px var(--space-2); }
.wk-sum tfoot td { padding: 8px var(--space-2); border-top: 1px solid var(--border-strong); font-weight: 600; }
.wk-sum td:first-child, .wk-sum th:first-child { padding-left: 0; white-space: nowrap; }
.wk-sum td:last-child, .wk-sum th:last-child { padding-right: 0; }
.wk-sum td:first-child, .wk-sum th:first-child { padding-left: var(--space-2); white-space: nowrap; }
.wk-sum td:last-child, .wk-sum th:last-child { padding-right: var(--space-2); }
.key { display: inline-flex; align-items: center; gap: 6px; }
.key i { width: 8px; height: 8px; display: inline-block; border-radius: var(--radius-xs); }
.k-att { background: var(--border-strong); }
@ -373,7 +378,7 @@ tbody tr:hover { background: var(--muted); }
background: none; color: var(--muted-foreground);
border: 1px solid var(--border-strong); border-radius: var(--radius);
height: 28px; padding: 0 10px;
transition: all 100ms ease;
transition: background-color 100ms ease, border-color 100ms ease, color 100ms ease;
white-space: nowrap;
}
.give-btn:hover { border-color: var(--foreground); color: var(--foreground); }
@ -387,7 +392,7 @@ tbody tr:hover { background: var(--muted); }
align-items: start;
}
.cal { table-layout: fixed; }
.cal th { text-align: center; font-size: var(--step-1); height: 32px; padding: 0; }
.cal th { text-align: center; font-size: var(--step-1); height: 32px; padding-inline: var(--space-2); }
.cal-day {
height: 76px; padding: var(--space-1);
border: 1px solid var(--border);
@ -454,6 +459,8 @@ tbody tr:hover { background: var(--muted); }
/* ── 모달 ── */
.modal {
position: fixed;
inset: 0;
margin: auto;
width: min(94vw, 860px);
max-height: min(86dvh, 760px);
@ -463,8 +470,11 @@ tbody tr:hover { background: var(--muted); }
background: var(--card);
color: var(--foreground);
overflow: hidden;
overscroll-behavior: contain;
box-shadow: var(--shadow-xl);
}
html:has(dialog.modal[open]) { overflow: hidden; }
.modal::backdrop { background: var(--backdrop-dim); backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px); }
@media (prefers-reduced-motion: no-preference) {
.modal[open] { animation: modal-in 200ms cubic-bezier(0.16, 1, 0.3, 1) both; }
@ -613,6 +623,8 @@ tbody tr:hover { background: var(--muted); }
padding-top: max(0px, var(--safe-top));
}
.topbar .brand { display: flex; flex: none; padding: 0; }
.topbar-right { flex-wrap: wrap; justify-content: flex-end; gap: var(--space-1); }
.topbar-right > * { min-width: 0; }
.crumb { display: none; }
.main { padding-bottom: calc(72px + var(--safe-bottom)); } /* 바텀 내비 + 홈 인디케이터 확보 */
.dash-grid, .cal-layout, .counsel-grid { grid-template-columns: minmax(0, 1fr); }
@ -620,7 +632,7 @@ tbody tr:hover { background: var(--muted); }
.dist-body { grid-template-columns: minmax(0, 1fr); }
.view-head { flex-direction: column; align-items: flex-start; gap: var(--space-2); padding: var(--space-5) var(--space-4) var(--space-4); }
.view > *:not(.view-head) { margin-inline: var(--space-4); }
.headline { gap: var(--space-2); }
.headline { gap: 0; }
.term-badge { display: none; }
.cal-day { height: 56px; }
}
@ -634,24 +646,26 @@ tbody tr:hover { background: var(--muted); }
.mcover-mark { font-size: 5rem; }
.msheet { padding: var(--space-4); min-height: 0; }
.headline { grid-template-columns: minmax(0, 1fr); }
.headline > div { border-right: 0; border-bottom: 1px solid var(--record-rule); }
.headline > div:last-child { border-bottom: 0; }
}
/* ── 알림 센터 ── */
.bell-wrap { position: relative; }
.bell {
position: relative; width: 32px; height: 32px;
position: relative; width: 44px; height: 44px;
display: inline-flex; align-items: center; justify-content: center;
border-radius: var(--radius); border: none; background: none; cursor: pointer;
color: var(--muted-foreground); transition: background-color 80ms ease, color 80ms ease;
}
.bell:hover, .bell.is-open { background: var(--muted-2); color: var(--foreground); }
.bell-dot {
position: absolute; top: 4px; right: 4px;
position: absolute; top: 8px; right: 8px;
width: 8px; height: 8px; border-radius: var(--radius-pill);
background: var(--danger); border: 2px solid var(--background);
}
.bell-menu {
display: none; position: absolute; top: 40px; right: 0; z-index: 40;
display: none; position: absolute; top: 52px; right: 0; z-index: 40;
width: min(340px, calc(100vw - 32px));
background: var(--card); border: 1px solid var(--border);
border-radius: var(--radius-lg); box-shadow: var(--shadow-lg); overflow: hidden;
@ -679,6 +693,21 @@ tbody tr:hover { background: var(--muted); }
.ntc.is-read p { color: var(--muted-foreground); }
.ntc time { display: block; font-size: var(--step--1); color: var(--faint); margin-top: 2px; }
/* 좁거나 낮은 화면에서는 종 모서리가 아니라 뷰포트가 알림판의 기준이다.
상대 위치를 유지하면 340px 패널이 좌측으로 밀리고 긴 목록이 하단 내비 아래로 사라진다. */
@media (max-width: 680px) {
.bell-menu {
position: fixed;
top: calc(56px + var(--safe-top) + var(--space-2));
right: max(var(--space-3), var(--safe-right));
bottom: calc(72px + var(--safe-bottom) + var(--space-2));
left: max(var(--space-3), var(--safe-left));
width: auto;
max-height: none;
overflow-y: auto;
}
}
/* ── 태그 칩 (유형·사유) ── */
.tag {
display: inline-flex; align-items: center; white-space: nowrap;
@ -920,15 +949,15 @@ kbd {
border-top: 1px solid var(--border);
padding: 6px max(var(--space-2), var(--safe-left)) calc(6px + var(--safe-bottom)) max(var(--space-2), var(--safe-right));
}
@media (max-width: 900px) { .bottom-nav { display: grid; grid-auto-flow: column; } }
@media (max-width: 900px) { .bottom-nav { display: grid; min-width: 0; grid-template-columns: repeat(5, minmax(0, 1fr)); grid-auto-flow: row; } }
.bnav-item {
appearance: none; background: none; border: 0; cursor: pointer; font: inherit;
display: grid; justify-items: center; align-content: center; gap: 3px;
min-height: 52px; min-width: 48px; padding: 4px 2px;
min-height: 52px; min-width: 0; padding: 4px 2px;
border-radius: var(--radius);
color: var(--muted-foreground);
}
.bnav-item span { font-size: var(--step--2); font-weight: 500; }
.bnav-item span { max-width: 100%; font-size: var(--step--2); font-weight: 500; overflow-wrap: anywhere; }
.bnav-item.is-current { color: var(--accent-ink); font-weight: 600; }
.bnav-item.is-current svg { color: var(--accent); }

View file

@ -90,6 +90,11 @@
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
--record-radius: 8px;
--record-rule: var(--border);
--record-body-size: 0.9375rem;
--record-meta-size: 0.8125rem;
/* 안전 영역 — viewport-fit=cover 선언 후에만 0이 아닌 값이 온다(WebKit 공식·MDN env()) */
--safe-top: env(safe-area-inset-top, 0px);
--safe-bottom: env(safe-area-inset-bottom, 0px);

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View file

@ -0,0 +1,31 @@
# 철판클럽 — 2026-09-12 화면 보정 기록
## 구도·타입·색 관찰
- 코발트, 큰 고딕 제목, 버거 사진은 브랜드의 속도와 조리 장면을 만드는 기존 자산이다.
- 기준 첫 화면의 파란 상품 카드는 절대 배치로 사진 프레임 밖에 걸쳐 캡션과 조합 시작 행동의 읽기 순서를 흐린다.
- 메뉴 이미지는 이미 4:3 표면이지만 크롭 기준이 명시되지 않았고, 조합·가맹의 조건과 비용은 값보다 장식 카드처럼 보일 위험이 있다.
## 근거 확인 범위
- `outputs/full-showcase-upgrade-20260912/screenshots/griddle-club/1440x1000-first.png`
- 소비자 홈, `franchise/`, 기존 메뉴·조합·가상 매장·가맹 조건의 ID와 스크립트
- 가상·생성 이미지·미전송·비용 미정·실제 주문 불가 표기는 보존한다.
## 결정 / 기각 조건
- 상품·예시 가격·조합 시작 버튼은 사진 아래 정상 흐름의 코발트 shelf로 둬 캡션과 겹치지 않게 한다.
- 메뉴 이미지는 중앙 `cover` 크롭을 통일하고, 조합기는 라벨·선택·가격 보조 정보를 같은 위계로 둔다.
- 가맹 절차와 비용은 단계·상태·다음 확인의 원장으로 정렬한다. 수익, 기간, 비용, 운영 성과를 새로 만들거나 추정하는 변경은 기각한다.
## 토큰·규칙
- 기존 `--blue`, `--paper-*`, `--ink`, `--line`, `--font-mono`만 사용한다.
- 상품 shelf와 제어 요소는 44px 이상의 동작 영역과 기존 focus 계약을 유지한다.
- 320px과 200% 확대에서 `min-width: 0`, 가로 스크롤 표, normal flow로 정보를 보존한다.
## 2026-09-12 현재 검증 기록
- `verify-final` 18개 묶음은 16개가 즉시 통과했다. 전체 묶음의 invariant·visual 검사는 Gaon·Dure를 대상으로 하며, 나머지 2개는 수정 뒤 release 독립 재실행에서 ALL PASS·ALL MATCH였다.
- root는 28개 모바일, 21개 데스크톱, 7개 하위 경로와 30개 앱 상태를 직접 검토해 수용했다. stress 112건은 hard failure 0이며, 가로 넘침 4건은 targeted fix 뒤 390px 재측정에서 0이다.
- 이 기록은 렌더·계약 범위의 현재 결과다. 모든 폭·키보드 경로·인쇄 등 개별 기능을 완전 재실행했다는 뜻이 아니며, 참여자 과업 효과도 미측정이다.

View file

@ -6,12 +6,12 @@
<title>철판클럽 가맹 안내 — 운영 조건부터 확인하기</title>
<meta
name="description"
content="가상 버거 브랜드 철판클럽의 상권, 점포, 면적, 개설 절차와 비용 미정 항목을 먼저 살펴보는 가맹 문의 쇼케이스입니다."
content="가상 버거 브랜드 철판클럽의 상권, 점포, 면적, 개설 절차와 비용 미정 항목을 살펴보고 가맹 검토에 필요한 조건을 정리해 보세요. 실제 상담과 계약은 제공하지 않습니다."
/>
<meta property="og:title" content="철판클럽 가맹 안내 — 운영 조건부터" />
<meta
property="og:description"
content="수익 약속 없이 상권, 점포, 주방 동선과 비용 미정 항목을 먼저 보여 주는 국내형 가맹 안내 화면."
content="수익 약속 없이 상권, 점포, 주방 동선과 비용 미정 항목을 안내합니다."
/>
<meta property="og:type" content="website" />
<meta property="og:image" content="../og.png" />
@ -20,7 +20,7 @@
<meta name="twitter:title" content="철판클럽 가맹 안내 — 운영 조건부터" />
<meta
name="twitter:description"
content="상권, 점포, 주방 동선과 비용 미정 항목을 먼저 살펴보는 가맹 안내 쇼케이스."
content="상권, 점포, 주방 동선과 비용 미정 항목을 살펴보는 가맹 안내."
/>
<meta name="twitter:image" content="../og.png" />
<meta name="theme-color" content="#fffdf8" />
@ -35,9 +35,9 @@
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@500;600&display=swap"
/>
<link rel="stylesheet" href="../styles/tokens.css?v=1" />
<link rel="stylesheet" href="../styles/site.css?v=1" />
<link rel="stylesheet" href="../styles/franchise.css?v=1" />
<link rel="stylesheet" href="../styles/tokens.css?v=20260912all" />
<link rel="stylesheet" href="../styles/site.css?v=20260912all" />
<link rel="stylesheet" href="../styles/franchise.css?v=20260912all" />
<link
rel="icon"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%232147e7'/%3E%3Cpath d='M6 9h20M8 16h16M10 23h12' stroke='%23fffdf8' stroke-width='3' stroke-linecap='square'/%3E%3C/svg%3E"
@ -49,7 +49,7 @@
<a class="skip-link" href="#main">본문으로 건너뛰기</a>
<div class="truth-bar" role="note">
<p>쇼케이스용 가상 브랜드 · 실제 가맹 모집, 상담, 계약을 제공하지 않음</p>
<p>가상 브랜드 안내 · 실제 가맹 모집, 상담, 계약을 제공하지 않습니다.</p>
</div>
<header class="site-head">
@ -64,7 +64,7 @@
<a href="#process">개설 절차</a>
<a href="#cost">비용 항목</a>
</nav>
<a class="head-action" href="#inquiry">문의 준비하기</a>
<a class="head-action" href="#inquiry">조건 정리하기</a>
</div>
</header>
@ -82,7 +82,7 @@
</div>
<div class="franchise-truth">
<strong>먼저 확인해 주세요</strong>
<p>실제 가맹 모집이 아닌 디자인 쇼케이스입니다. 금액, 수익, 매장 수와 개점 기간을 꾸며내지 않습니다.</p>
<p>금액, 수익, 매장 수와 개점 기간은 확인된 정보가 없어 표시하지 않습니다.</p>
</div>
</div>
<figure class="franchise-hero-media image-shell">
@ -166,7 +166,7 @@
<div class="wrap">
<div class="section-head split-head">
<div class="process-heading-copy"><p class="section-kicker">개설 절차</p><h2 id="process-heading-franchise">기간을 꾸미지 않고<br />순서만 보여 줍니다.</h2></div>
<p class="section-note">점포, 자료, 협의와 공사 조건에 따라 기간이 달라집니다. 아래 흐름은 화면 설명용 예시이며 계약이나 개점을 보장하지 않습니다.</p>
<p class="section-note">점포, 자료, 협의와 공사 조건에 따라 기간이 달라집니다. 아래 순서는 검토에 필요한 항목을 정리한 것이며 계약이나 개점을 보장하지 않습니다.</p>
</div>
<ol class="franchise-timeline">
<li><span>01</span><div><h3>문의 준비</h3><p>희망 지역과 시작 시기를 정리합니다.</p></div><small>준비 전</small></li>
@ -219,17 +219,17 @@
<section class="section inquiry-section" id="inquiry" aria-labelledby="inquiry-heading">
<div class="wrap inquiry-layout">
<div class="inquiry-intro">
<p class="section-kicker">문의 준비 체험</p>
<p class="section-kicker">조건 정리</p>
<h2 id="inquiry-heading">연락처 없이도<br />조건부터 정리하기.</h2>
<p>실제 이름과 연락처를 받지 않습니다. 화면 안에서 상권과 점포 조건을 정리하고 전송 전 검토 상태를 체험합니다.</p>
<p>실제 이름과 연락처를 받지 않습니다. 가상의 운영 조건을 입력하면 어떤 항목을 확인해야 하는지 순서대로 살펴볼 수 있습니다.</p>
<div class="privacy-note" role="note">
<strong>실제 개인정보를 입력하지 마세요.</strong>
<p>입력 내용은 서버로 보내거나 저장하지 않습니다. 성공 화면도 실제 접수가 아닙니다.</p>
<p>입력한 내용은 전송하거나 보관하지 않습니다. 조건을 확인해도 상담 신청은 이루어지지 않습니다.</p>
</div>
</div>
<div class="inquiry-panel">
<ol class="step-indicator" aria-label="문의 준비 단계">
<ol class="step-indicator" aria-label="조건 정리 단계">
<li class="is-current" data-step-indicator="1" aria-current="step"><span>1</span><small>상권</small></li>
<li data-step-indicator="2"><span>2</span><small>점포</small></li>
<li data-step-indicator="3"><span>3</span><small>검토</small></li>
@ -240,8 +240,8 @@
<p class="step-count">1 / 3</p>
<h3 id="step-one-heading">어느 지역에서 시작할까요?</h3>
<div class="form-field">
<label for="demo-name">예시 호칭</label>
<input id="demo-name" name="demoName" type="text" minlength="2" autocomplete="off" placeholder="실명 대신 예시 호칭" required />
<label for="demo-name">확인용 호칭</label>
<input id="demo-name" name="demoName" type="text" minlength="2" autocomplete="off" placeholder="실명 대신 두 글자 이상 입력" required />
<small>실제 이름을 입력하지 마세요.</small>
<p class="field-error" id="demo-name-error"></p>
</div>
@ -306,14 +306,14 @@
<p class="field-error" id="experience-error"></p>
</fieldset>
<div class="form-actions split-actions"><button class="button button-ghost" type="button" data-back>이전</button><button class="button button-primary" type="button" data-next>입력 내용 검토</button></div>
<div class="form-actions split-actions"><button class="button button-ghost" type="button" data-back>이전</button><button class="button button-primary" type="button" data-next>입력한 조건 검토</button></div>
</section>
<section class="form-step" data-step="3" aria-labelledby="step-three-heading">
<p class="step-count">3 / 3</p>
<h3 id="step-three-heading">보내기 전에 확인하세요.</h3>
<h3 id="step-three-heading">입력한 조건을 확인하세요.</h3>
<dl class="review-list" id="review-list">
<div><dt>예시 호칭</dt><dd data-review="demoName">—</dd></div>
<div><dt>확인용 호칭</dt><dd data-review="demoName">—</dd></div>
<div><dt>희망 지역</dt><dd data-review="region">—</dd></div>
<div><dt>희망 상권</dt><dd data-review="areaKind">—</dd></div>
<div><dt>점포 상태</dt><dd data-review="storePossession">—</dd></div>
@ -321,18 +321,18 @@
<div><dt>희망 시기</dt><dd data-review="openingWindow">—</dd></div>
<div><dt>운영 경험</dt><dd data-review="experience">—</dd></div>
</dl>
<label class="consent-check"><input type="checkbox" id="demo-consent" required /><span>실제 문의가 아니며 개인정보를 입력하지 않았음을 확인했습니다.</span></label>
<label class="consent-check"><input type="checkbox" id="demo-consent" required /><span>이 페이지에서는 상담을 신청할 수 없으며 개인정보를 입력하지 않았습니다.</span></label>
<p class="field-error" id="demo-consent-error"></p>
<div class="form-actions split-actions"><button class="button button-ghost" type="button" data-back>수정하기</button><button class="button button-primary" type="submit" id="submit-inquiry">문의 준비 마치기</button></div>
<div class="form-actions split-actions"><button class="button button-ghost" type="button" data-back>수정하기</button><button class="button button-primary" type="submit" id="submit-inquiry">입력한 조건 확인하기</button></div>
<p class="submit-state" id="submit-state" role="alert"></p>
</section>
</form>
<section class="inquiry-success" id="inquiry-success" hidden aria-labelledby="success-heading" tabindex="-1">
<span class="success-mark" aria-hidden="true">✓</span>
<p class="section-kicker">화면 체험 완료</p>
<h3 id="success-heading">문의 준비가 끝났어요.</h3>
<p>제출 절차를 보여 주는 예시이며 실제 담당자에게 전송되지 않았습니다.</p>
<p class="section-kicker">조건 확인 완료</p>
<h3 id="success-heading">입력한 조건을 확인했습니다.</h3>
<p>입력한 내용은 전송되거나 보관되지 않으며, 상담 신청은 이루어지지 않습니다.</p>
<div class="success-actions"><button class="button button-ghost" type="button" id="review-again">입력 내용 다시 보기</button><button class="button button-primary" type="button" id="restart-form">처음부터 보기</button></div>
</section>
</div>
@ -351,7 +351,7 @@
<div class="wrap foot-grid franchise-foot-grid">
<div><a class="brand brand-foot" href="../"><span class="brand-mark" aria-hidden="true"><i></i><i></i><i></i></span><span>철판클럽</span></a><p>눌러 굽고, 바로 건넨다.</p></div>
<div><strong>가맹 안내</strong><a href="#model">운영 방식</a><a href="#site-fit">상권과 점포</a><a href="#process">개설 절차</a><a href="#cost">비용 항목</a></div>
<div class="foot-truth"><strong>쇼케이스 고지</strong><p>가상 브랜드이며 실제 가맹 모집, 수익 안내, 계약과 문의 전송을 제공하지 않습니다.</p><p class="mono">DESIGN SHOWCASE · 2026</p></div>
<div class="foot-truth"><strong>가상 브랜드 안내</strong><p>실제 가맹 모집, 수익 안내, 계약과 상담 신청을 제공하지 않습니다.</p><p class="mono">GRIDDLE CLUB · 2026</p></div>
</div>
</footer>
</body>

View file

@ -6,12 +6,12 @@
<title>철판클럽 — 눌러 굽고, 바로 건넨다</title>
<meta
name="description"
content="서울의 빠른 한 끼를 위한 가상 눌림 버거 브랜드 철판클럽의 메뉴, 조합기, 예시 매장과 운영 방식을 살펴보세요."
content="가상 버거 브랜드 철판클럽에서 메뉴를 고르고 버거를 조합하며, 가상 매장의 이용 방식과 운영 콘셉트를 살펴보세요. 실제 주문과 결제는 제공하지 않습니다."
/>
<meta property="og:title" content="철판클럽 — 눌러 굽고, 바로 건넨다" />
<meta
property="og:description"
content="메뉴를 고르고, 재료를 조합하고, 예시 매장 운영 상태까지 확인하는 국내형 버거 쇼케이스."
content="메뉴를 고르고 재료를 조합하며, 가상 매장의 운영 방식을 살펴보는 버거 브랜드 콘셉트."
/>
<meta property="og:type" content="website" />
<meta property="og:image" content="./og.png" />
@ -20,7 +20,7 @@
<meta name="twitter:title" content="철판클럽 — 눌러 굽고, 바로 건넨다" />
<meta
name="twitter:description"
content="메뉴를 고르고 재료를 조합해 보는 국내형 버거 브랜드 쇼케이스."
content="메뉴를 고르고 재료를 조합해 보는 가상 버거 브랜드 철판클럽."
/>
<meta name="twitter:image" content="./og.png" />
<meta name="theme-color" content="#f4f1e9" />
@ -35,20 +35,20 @@
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@500;600&display=swap"
/>
<link rel="stylesheet" href="./styles/tokens.css?v=1" />
<link rel="stylesheet" href="./styles/site.css?v=1" />
<link rel="stylesheet" href="./styles/tokens.css?v=20260912all" />
<link rel="stylesheet" href="./styles/site.css?v=20260912all" />
<link
rel="icon"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%232147e7'/%3E%3Cpath d='M6 9h20M8 16h16M10 23h12' stroke='%23fffdf8' stroke-width='3' stroke-linecap='square'/%3E%3C/svg%3E"
/>
<script>document.documentElement.classList.add("is-js");</script>
<script src="./scripts/site.js?v=1" defer></script>
<script src="./scripts/site.js?v=2" defer></script>
</head>
<body>
<a class="skip-link" href="#main">본문으로 건너뛰기</a>
<div class="truth-bar" role="note">
<p>쇼케이스용 가상 브랜드 · 가격과 매장 정보는 화면 동작 확인용 예시</p>
<p>가상 브랜드 안내 · 가격과 매장 정보는 실제 판매 정보가 아닙니다.</p>
</div>
<header class="site-head" id="top">
@ -71,7 +71,7 @@
<section class="hero" aria-labelledby="hero-heading">
<div class="hero-copy">
<p class="section-kicker">서울 도심 눌림 버거</p>
<h1 id="hero-heading">눌러 굽고,<br />바로 건넨다.</h1>
<h1 id="hero-heading">눌러 굽고,<span class="hero-wide-space" aria-hidden="true"> </span><br />바로 건넨다.</h1>
<p class="hero-lead">
얇게 눌러 가장자리는 바삭하게. 주문 뒤 조립하고 포장해, 퇴근길 한 상자로 건넵니다.
</p>
@ -124,10 +124,10 @@
<div class="section-head split-head">
<div>
<p class="section-kicker">오늘 고를 메뉴</p>
<h2 id="menu-heading">버거가 먼저 보이게.</h2>
<h2 id="menu-heading">메뉴를 고르고<br />내 버거를 조합해 보세요.</h2>
</div>
<p class="section-note">
모든 가격은 조합기 동작을 보여 주기 위한 예시입니다. 실제 판매와 주문은 이루어지지 않습니다.
표시된 메뉴와 가격은 브랜드 콘셉트를 설명하기 위한 값입니다. 이 페이지에서는 실제 판매와 주문을 제공하지 않습니다.
</p>
</div>
@ -162,9 +162,17 @@
</article>
<article class="menu-item menu-item-graphic" data-menu-category="burger">
<div class="ingredient-stack" aria-hidden="true">
<span class="bun"></span><span class="pickle"></span><span class="cheese"></span><span class="patty"></span><span class="bun bottom"></span>
<b>02</b>
<div class="menu-photo image-shell">
<img
src="./assets/single-smash-generated.webp"
width="960"
height="720"
alt="코발트색 트레이 위에 치즈와 피클을 넣은 싱글 스매시 버거"
loading="lazy"
/>
<span class="image-fallback" aria-hidden="true">철판 싱글 사진</span>
<span class="menu-number">02</span>
<span class="menu-image-note">생성 이미지</span>
</div>
<div class="menu-body">
<div><p class="menu-state">가볍게</p><h3>철판 싱글</h3></div>
@ -224,9 +232,9 @@
<div class="section-head split-head">
<div>
<p class="section-kicker">버거 조합기</p>
<h2 id="builder-heading">상자에 담길 내용을<br />먼저 정하세요.</h2>
<h2 id="builder-heading">내 버거 조합을<br />바꿔 보세요.</h2>
</div>
<p class="section-note">재료 선택과 예시 총액은 실제로 바뀝니다. 주문 전송 기능은 없습니다.</p>
<p class="section-note">아래 구성은 메뉴를 살펴보기 위한 예시입니다. 재료를 바꾸면 오른쪽 안내 카드와 예시 가격이 함께 바뀝니다.</p>
</div>
<div class="builder-layout">
@ -288,14 +296,14 @@
</fieldset>
<fieldset>
<legend><span>05</span> 받을 예시 매장</legend>
<legend><span>05</span> 확인할 가상 매장</legend>
<label class="field-label" for="builder-store">매장</label>
<select id="builder-store" name="store">
<option value="seongsu">성수 예시점 · 영업 중 예시</option>
<option value="yeonnam">연남 예시점 · 영업 종료 예시</option>
<option value="mullae">문래 예시점 · 오늘은 쉬어요</option>
<option value="seongsu">성수점 · 영업 중</option>
<option value="yeonnam">연남점 · 영업 종료</option>
<option value="mullae">문래점 · 오늘 휴무</option>
</select>
<p class="field-help">매장과 영업 상태도 화면 동작용 예시입니다.</p>
<p class="field-help">아래 매장과 운영 상태는 실제 방문·주문 정보가 아닙니다.</p>
</fieldset>
</form>
@ -309,27 +317,25 @@
loading="lazy"
/>
<span class="image-fallback" aria-hidden="true">조합 버거 사진</span>
<span class="summary-stamp">화면 예시</span>
<span class="summary-stamp">가상 메뉴</span>
</div>
<div class="summary-body">
<p class="section-kicker">고른 내용</p>
<p class="section-kicker">선택 결과</p>
<h3 id="summary-heading">철판 더블</h3>
<p id="summary-lines">패티 두 장 / 치즈 한 장 / 양파 기본 / 피클 기본 / 소스 기본</p>
<dl class="summary-list">
<div><dt>받기</dt><dd id="summary-serving">단품</dd></div>
<div><dt>예시 매장</dt><dd id="summary-store">성수 예시점</dd></div>
<div><dt>상태</dt><dd id="summary-store-state">영업 중 예시</dd></div>
<div><dt>가상 매장</dt><dd id="summary-store">성수점</dd></div>
<div><dt>상태</dt><dd id="summary-store-state">영업 중</dd></div>
</dl>
<div class="allergen-note" id="allergen-note">
<strong>알레르기 예시</strong>
<p id="summary-allergens">알, 우유, 대두, 밀, 쇠고기</p>
<small>같은 철판과 조리 도구를 함께 씁니다. 재료를 빼도 알레르기 안전을 보장하지 않습니다.</small>
</div>
<div class="summary-total"><span>화면 계산용 예시 총액</span><strong id="summary-total">8,900원</strong></div>
<p class="truth-inline">실제 주문이나 결제로 전송되지 않습니다.</p>
<button class="button button-primary button-full" type="button" id="save-builder">선택 내용 저장</button>
<a class="button button-ghost button-full" href="#stores" id="other-store-link">다른 매장 찾기</a>
<p class="save-status" id="save-status" role="status" aria-live="polite"></p>
<div class="summary-total"><span>예시 총액</span><strong id="summary-total">8,900원</strong></div>
<p class="truth-inline">이 페이지는 메뉴 조합을 살펴보는 안내 페이지이며, 주문과 결제는 진행되지 않습니다.</p>
<a class="button button-primary button-full" href="#stores" id="check-store-link">가상 매장 안내 보기</a>
</div>
</aside>
</div>
@ -364,13 +370,13 @@
<section class="section stores-section" id="stores" aria-labelledby="stores-heading">
<div class="wrap">
<div class="section-head split-head">
<div><p class="section-kicker">매장 찾기</p><h2 id="stores-heading">지금 받을 수 있는지<br />먼저 확인하세요.</h2></div>
<p class="section-note">주소, 전화, 거리와 운영 상태를 꾸며내지 않습니다. 아래 세 곳은 상태 화면을 위한 예시 매장입니다.</p>
<div><p class="section-kicker">가상 매장 안내</p><h2 id="stores-heading">매장별 이용 방식은<br />이렇게 안내합니다.</h2></div>
<p class="section-note">주소, 전화, 거리와 운영 상태를 임의로 만들지 않습니다. 아래 세 곳은 브랜드 콘셉트를 설명하는 가상 매장입니다.</p>
</div>
<div class="store-tools">
<div class="search-box">
<input id="store-search" type="search" placeholder="동네 또는 매장명 검색" autocomplete="off" aria-label="예시 매장 검색" />
<input id="store-search" type="search" placeholder="동네 또는 매장명 검색" autocomplete="off" aria-label="가상 매장 검색" />
<button type="button" id="clear-store-search" hidden>지우기</button>
</div>
<div class="region-filters" role="group" aria-label="지역 고르기">
@ -383,46 +389,46 @@
<div class="store-layout">
<div class="store-list" id="store-list">
<article class="store-card is-selected" data-store="seongsu" data-region="성동" data-search="성수 예시점 성동 성수동" tabindex="-1">
<div class="store-card-top"><span class="store-state state-open">영업 중 예시</span><span>예시 매장</span></div>
<h3>성수 예시점</h3>
<article class="store-card is-selected" data-store="seongsu" data-region="성동" data-search="성수점 성동 성수동" tabindex="-1">
<div class="store-card-top"><span class="store-state state-open">영업 중</span><span>가상 매장</span></div>
<h3>성수점</h3>
<p>서울 성동구 성수동 일대 · 상세 주소 없음</p>
<dl><div><dt>운영</dt><dd>예시 11:00–21:30</dd></div><div><dt>마지막 주문</dt><dd>예시 21:00</dd></div></dl>
<ul aria-label="이용 방식"><li>포장 가능 예시</li><li>배달 가능 예시</li></ul>
<button type="button" data-select-store="seongsu">이 매장으로 조합 보기</button>
<dl><div><dt>운영</dt><dd>11:00–21:30</dd></div><div><dt>마지막 주문</dt><dd>21:00</dd></div></dl>
<ul aria-label="이용 방식"><li>포장 가능</li><li>배달 가능</li></ul>
<button type="button" data-select-store="seongsu">성수점으로 조합 보기</button>
</article>
<article class="store-card" data-store="yeonnam" data-region="마포" data-search="연남 예시점 마포 연남동" tabindex="-1">
<div class="store-card-top"><span class="store-state state-closed">영업 종료 예시</span><span>예시 매장</span></div>
<h3>연남 예시점</h3>
<article class="store-card" data-store="yeonnam" data-region="마포" data-search="연남점 마포 연남동" tabindex="-1">
<div class="store-card-top"><span class="store-state state-closed">영업 종료</span><span>가상 매장</span></div>
<h3>연남점</h3>
<p>서울 마포구 연남동 일대 · 상세 주소 없음</p>
<dl><div><dt>운영</dt><dd>예시 11:30–20:30</dd></div><div><dt>마지막 주문</dt><dd>오늘 마감</dd></div></dl>
<dl><div><dt>운영</dt><dd>11:30–20:30</dd></div><div><dt>마지막 주문</dt><dd>오늘 마감</dd></div></dl>
<ul aria-label="이용 방식"><li>포장 마감</li><li>배달 마감</li></ul>
<button type="button" data-select-store="yeonnam">종료 상태로 조합 보기</button>
<button type="button" data-select-store="yeonnam">연남점으로 조합 보기</button>
</article>
<article class="store-card" data-store="mullae" data-region="영등포" data-search="문래 예시점 영등포 문래동" tabindex="-1">
<div class="store-card-top"><span class="store-state state-pause">오늘은 쉬어요</span><span>예시 매장</span></div>
<h3>문래 예시점</h3>
<article class="store-card" data-store="mullae" data-region="영등포" data-search="문래점 영등포 문래동" tabindex="-1">
<div class="store-card-top"><span class="store-state state-pause">오늘 휴무</span><span>가상 매장</span></div>
<h3>문래점</h3>
<p>서울 영등포구 문래동 일대 · 상세 주소 없음</p>
<dl><div><dt>운영</dt><dd>임시 휴무 예시</dd></div><div><dt>다음 확인</dt><dd>매장별 안내 필요</dd></div></dl>
<dl><div><dt>운영</dt><dd>임시 휴무</dd></div><div><dt>다음 확인</dt><dd>매장별 안내 필요</dd></div></dl>
<ul aria-label="이용 방식"><li>포장 불가</li><li>배달 불가</li></ul>
<button type="button" data-select-store="mullae">휴무 상태로 조합 보기</button>
<button type="button" data-select-store="mullae">문래점으로 조합 보기</button>
</article>
</div>
<aside class="store-map" aria-label="예시 매장 분포 그림">
<aside class="store-map" aria-label="가상 매장 분포 그림">
<div class="map-grid" aria-hidden="true"></div>
<span class="map-river" aria-hidden="true"></span>
<button type="button" class="map-pin pin-seongsu is-active" data-map-store="seongsu" aria-label="성수 예시점 고르기"><span>성수</span></button>
<button type="button" class="map-pin pin-yeonnam" data-map-store="yeonnam" aria-label="연남 예시점 고르기"><span>연남</span></button>
<button type="button" class="map-pin pin-mullae" data-map-store="mullae" aria-label="문래 예시점 고르기"><span>문래</span></button>
<p>실제 지도와 위치가 아닌 화면 설명용 도식</p>
<button type="button" class="map-pin pin-seongsu is-active" data-map-store="seongsu" aria-label="성수점 고르기"><span>성수</span></button>
<button type="button" class="map-pin pin-yeonnam" data-map-store="yeonnam" aria-label="연남점 고르기"><span>연남</span></button>
<button type="button" class="map-pin pin-mullae" data-map-store="mullae" aria-label="문래점 고르기"><span>문래</span></button>
<p>위치 안내가 아닌 지역별 이용 상태를 보여 주는 그림입니다.</p>
</aside>
</div>
<div class="empty-state store-empty" id="store-empty" hidden role="status">
<strong>찾는 조건과 맞는 예시 매장이 없어요.</strong>
<strong>찾는 조건과 맞는 가상 매장이 없어요.</strong>
<p>검색어와 지역 조건을 지우고 다시 확인해 주세요.</p>
<button type="button" id="reset-store-search">검색 조건 지우기</button>
</div>
@ -445,7 +451,7 @@
<h2 id="franchise-heading">매출 약속보다<br />운영 조건부터.</h2>
<p>상권, 점포, 면적, 조리 동선과 비용 미정 항목을 먼저 확인할 수 있게 분리했습니다.</p>
<a class="button button-light" href="./franchise/">가맹 운영 조건 보기 <span aria-hidden="true">→</span></a>
<small>실제 가맹 모집과 상담이 아닌 쇼케이스용 흐름입니다.</small>
<small>실제 가맹 모집과 상담을 제공하지 않습니다.</small>
</div>
</section>
</main>
@ -453,9 +459,9 @@
<footer class="site-foot">
<div class="wrap foot-grid">
<div><a class="brand brand-foot" href="#top"><span class="brand-mark" aria-hidden="true"><i></i><i></i><i></i></span><span>철판클럽</span></a><p>눌러 굽고, 바로 건넨다.</p></div>
<div><strong>살펴보기</strong><a href="#menu">메뉴</a><a href="#builder">조합기</a><a href="#stores">예시 매장</a><a href="./franchise/">가맹 안내</a></div>
<div><strong>살펴보기</strong><a href="#menu">메뉴</a><a href="#builder">조합하기</a><a href="#stores">가상 매장 안내</a><a href="./franchise/">가맹 안내</a></div>
<div><strong>정보 안내</strong><p>알레르기 정보도 예시입니다.</p><p>원산지와 영양 정보는 실제 제품 자료가 아닙니다.</p></div>
<div class="foot-truth"><strong>쇼케이스 고지</strong><p>가상 브랜드이며 실제 주문, 결제, 매장 방문, 가맹 문의를 제공하지 않습니다.</p><p class="mono">DESIGN SHOWCASE · 2026</p></div>
<div class="foot-truth"><strong>가상 브랜드 안내</strong><p>실제 주문, 결제, 매장 방문, 가맹 문의를 제공하지 않습니다.</p><p class="mono">GRIDDLE CLUB · 2026</p></div>
</div>
</footer>
</body>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 477 KiB

After

Width:  |  Height:  |  Size: 500 KiB

Before After
Before After

View file

@ -105,7 +105,7 @@
clearRadioError("areaKind", "area-kind-error");
if (getInputValue("demoName").length < 2) {
invalid.push(setFieldError("demo-name", "demo-name-error", "예시 호칭을 두 글자 이상 입력해 주세요."));
invalid.push(setFieldError("demo-name", "demo-name-error", "확인용 호칭을 두 글자 이상 입력해 주세요."));
}
if (!getSelectValue("region")) {
invalid.push(setFieldError("region", "region-error", "희망 지역을 골라 주세요."));
@ -240,14 +240,14 @@
if (!(consent instanceof HTMLInputElement) || !consent.checked) {
consent?.setAttribute("aria-invalid", "true");
consent?.setAttribute("aria-describedby", "demo-consent-error");
if (consentError) consentError.textContent = "문의 체험 안내를 확인해 주세요.";
if (consentError) consentError.textContent = "개인정보를 입력하지 않았는지 확인해 주세요.";
consent?.focus();
return;
}
if (submitButton instanceof HTMLButtonElement) {
submitButton.disabled = true;
submitButton.textContent = "화면 상태 확인 중";
submitButton.textContent = "입력한 조건 확인 중";
}
if (submitState) submitState.textContent = "";
@ -258,7 +258,7 @@
submitButton.textContent = "다시 시도하기";
}
if (submitState) {
submitState.textContent = "지금은 문의 준비 상태를 바꾸지 못했어요. 입력 내용은 화면에 그대로 두었습니다.";
submitState.textContent = "지금은 입력한 조건을 확인할 수 없어요. 입력한 값은 그대로 두었습니다.";
}
return;
}
@ -271,7 +271,7 @@
}
if (submitButton instanceof HTMLButtonElement) {
submitButton.disabled = false;
submitButton.textContent = "문의 준비 마치기";
submitButton.textContent = "입력한 조건 확인하기";
}
}, 450);
});

View file

@ -61,13 +61,10 @@
const summaryStoreState = document.querySelector("#summary-store-state");
const summaryAllergens = document.querySelector("#summary-allergens");
const summaryTotal = document.querySelector("#summary-total");
const saveButton = document.querySelector("#save-builder");
const saveStatus = document.querySelector("#save-status");
const stores = {
seongsu: { label: "성수 예시점", state: "영업 중 예시", available: true },
yeonnam: { label: "연남 예시점", state: "오늘 영업 종료 예시", available: false },
mullae: { label: "문래 예시점", state: "오늘은 쉬어요", available: false },
seongsu: { label: "성수점", state: "영업 중" },
yeonnam: { label: "연남점", state: "오늘 영업 종료" },
mullae: { label: "문래점", state: "오늘 휴무" },
};
const fieldValue = (name) => {
@ -134,16 +131,6 @@
if (summaryAllergens) summaryAllergens.textContent = allergens.join(", ");
if (summaryTotal) summaryTotal.textContent = formatWon(builderPrice({ flavor, patty, cheese, serving }));
if (saveButton) {
saveButton.disabled = soldOut || !store.available;
if (soldOut) saveButton.textContent = "오늘 품절 예시";
else if (!store.available) saveButton.textContent = "다른 매장을 골라 주세요";
else saveButton.textContent = "선택 내용 저장";
}
if (saveStatus) {
saveStatus.textContent = "";
saveStatus.classList.remove("is-error");
}
};
builder?.addEventListener("change", updateBuilder);
@ -169,20 +156,6 @@
button.addEventListener("click", () => applyPreset(button.dataset.preset || "double"));
});
saveButton?.addEventListener("click", () => {
if (!builder || !saveStatus) return;
const snapshot = Object.fromEntries(new FormData(builder).entries());
try {
if (fixture === "save-fail") throw new Error("fixture");
sessionStorage.setItem("griddle-club-demo-selection", JSON.stringify(snapshot));
saveStatus.classList.remove("is-error");
saveStatus.textContent = "이 기기의 현재 탭에 선택 내용을 남겼어요. 실제 주문으로 전송되지 않습니다.";
} catch {
saveStatus.classList.add("is-error");
saveStatus.textContent = "선택 내용을 남기지 못했어요. 고른 내용은 화면에 그대로 두었습니다.";
}
});
const storeCards = [...document.querySelectorAll("[data-store]")];
const mapPins = [...document.querySelectorAll("[data-map-store]")];
const storeSearch = document.querySelector("#store-search");

View file

@ -17,7 +17,7 @@
}
.franchise-hero-copy .section-kicker {
color: #8ea5ff;
color: var(--blue-soft);
}
.franchise-hero h1 {
@ -62,7 +62,7 @@
max-width: 560px;
margin-top: 52px;
padding: 18px 20px;
border-left: 3px solid #8ea5ff;
border-left: 3px solid var(--blue-soft);
background: #2b2926;
}
@ -299,9 +299,9 @@
.franchise-timeline li {
display: grid;
grid-template-columns: 56px 1fr auto;
grid-template-columns: 4.5rem minmax(0, 1fr) minmax(9rem, auto);
align-items: center;
gap: 28px;
gap: 1.5rem;
min-height: 124px;
padding: 22px 0;
border-bottom: 1px solid var(--line);
@ -325,10 +325,13 @@
}
.franchise-timeline small {
justify-self: end;
min-width: 8rem;
padding: 6px 9px;
border: 1px solid var(--line);
font-size: 10px;
font-weight: 700;
text-align: center;
}
.cost-section {
@ -337,7 +340,7 @@
}
.cost-section .section-kicker {
color: #8ea5ff;
color: var(--blue-soft);
}
.cost-head {
@ -355,6 +358,7 @@
.unknown-cost {
padding: 22px;
border: 1px solid #625f5a;
border-left: 3px solid var(--blue-soft);
background: #2a2825;
}
@ -366,7 +370,7 @@
.unknown-cost strong {
display: block;
margin-top: 3px;
color: #8ea5ff;
color: var(--blue-soft);
font-size: 25px;
}
@ -390,8 +394,10 @@
.cost-table th,
.cost-table td {
padding: 17px 14px;
padding: 1.125rem 1rem;
border-bottom: 1px solid #4f4c47;
font-size: 0.8125rem;
line-height: 1.55;
text-align: left;
}
@ -410,7 +416,7 @@
}
.cost-table td:nth-child(3) {
color: #8ea5ff;
color: var(--blue-soft);
font-family: var(--font-mono);
font-weight: 600;
}
@ -813,6 +819,12 @@
grid-template-columns: 1.2fr 0.8fr 1.4fr;
}
.cost-table th:nth-child(3),
.cost-table td:nth-child(3) {
font-variant-numeric: tabular-nums;
text-align: right;
}
@media (max-width: 960px) {
.franchise-hero {
grid-template-columns: 0.95fr 1.05fr;
@ -977,3 +989,38 @@
gap: 3px;
}
}
/* 공개 메인과 같은 초광폭 상한: 사진이 내용보다 먼저 커지지 않는다. */
@media (min-width: 120rem) {
.franchise-hero {
width: min(100%, var(--hero-wide));
min-height: clamp(43rem, 48vw, 54rem);
grid-template-columns: minmax(0, 1fr) minmax(42rem, 1.14fr);
margin-inline: auto;
border-inline: 1px solid var(--ink-soft);
}
.franchise-hero-copy {
width: min(100%, var(--copy-wide));
justify-self: end;
padding: clamp(5.5rem, 6vw, 8rem) clamp(4rem, 5vw, 6rem);
}
.franchise-hero h1 {
max-width: 12ch;
font-size: clamp(4.25rem, 3.2rem + 2.2vw, 5.5rem);
}
.franchise-hero-media {
width: min(100%, var(--media-wide));
min-height: clamp(43rem, 48vw, 54rem);
justify-self: end;
}
.fact-rail .wrap,
.model-section .wrap,
.site-fit-section .wrap,
.franchise-process .wrap {
max-width: var(--workbench-wide);
}
}

View file

@ -240,6 +240,17 @@ fieldset {
line-height: 0.98;
}
/* 초광폭에서 바깥 여백을 늘려도 카피 칼럼의 안쪽 폭이 사라지지 않게 한다. */
@media (min-width: 120rem) {
.hero {
grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
}
.hero-copy {
max-width: none;
}
}
.hero-lead {
max-width: 520px;
margin-top: 30px;
@ -373,8 +384,9 @@ fieldset {
.hero-order button {
justify-self: start;
margin-top: 12px;
padding: 0 0 3px;
min-height: 44px;
margin: 8px -8px -4px;
padding: 8px 8px 7px;
border: 0;
border-bottom: 1px solid white;
background: transparent;
@ -501,7 +513,6 @@ fieldset {
}
.menu-photo,
.ingredient-stack,
.menu-poster {
aspect-ratio: 4 / 3;
position: relative;
@ -512,6 +523,7 @@ fieldset {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center center;
}
.menu-number {
@ -527,57 +539,17 @@ fieldset {
font-size: 12px;
}
.ingredient-stack {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
background: var(--steel);
}
.ingredient-stack span {
width: 62%;
height: 9%;
display: block;
border-radius: 45% 45% 15% 15%;
background: var(--ink);
}
.ingredient-stack .bun {
height: 19%;
background: #d89643;
}
.ingredient-stack .bun.bottom {
height: 11%;
border-radius: 10% 10% 35% 35%;
}
.ingredient-stack .pickle {
height: 4%;
background: #4e6726;
}
.ingredient-stack .cheese {
width: 67%;
height: 5%;
background: var(--cheese);
transform: rotate(-2deg);
}
.ingredient-stack .patty {
height: 11%;
border-radius: 30%;
background: #4a2118;
}
.ingredient-stack b {
.menu-image-note {
position: absolute;
top: 14px;
left: 14px;
right: 10px;
bottom: 10px;
padding: 5px 7px;
background: rgb(255 253 248 / 94%);
color: var(--ink);
font-family: var(--font-mono);
font-size: 12px;
font-size: 10px;
font-weight: 600;
line-height: 1.2;
}
.menu-poster {
@ -697,7 +669,7 @@ fieldset {
}
.builder-section > .wrap > .section-head .section-kicker {
color: #8ea5ff;
color: var(--blue-soft);
}
.builder-summary .section-kicker {
@ -724,6 +696,10 @@ fieldset {
border-bottom: 1px solid #625f5a;
}
.builder-form fieldset + fieldset {
margin-top: 16px;
}
.builder-form legend {
width: 100%;
margin-bottom: 18px;
@ -733,7 +709,7 @@ fieldset {
.builder-form legend span {
margin-right: 12px;
color: #8ea5ff;
color: var(--blue-soft);
font-family: var(--font-mono);
font-size: 11px;
}
@ -776,7 +752,7 @@ fieldset {
}
.choice-row input:checked + span {
border-color: #8ea5ff;
border-color: var(--blue-soft);
background: var(--blue);
color: white;
}
@ -786,7 +762,7 @@ fieldset {
}
.choice-row input:focus-visible + span {
outline: 3px solid #8ea5ff;
outline: 3px solid var(--blue-soft);
outline-offset: 3px;
}
@ -992,18 +968,6 @@ fieldset {
background: transparent;
}
.save-status {
min-height: 23px;
margin-top: 10px;
color: var(--success);
font-size: 11px;
font-weight: 700;
}
.save-status.is-error {
color: var(--danger);
}
.process-section {
padding-block: 0;
background: var(--steel);
@ -1097,7 +1061,7 @@ fieldset {
.search-box input {
width: 100%;
min-height: 50px;
padding: 8px 0;
padding: 8px 12px;
border: 0;
border-radius: 0;
background: transparent;
@ -1109,7 +1073,15 @@ fieldset {
outline: 0;
}
.search-box:focus-within {
outline: 3px solid var(--focus);
outline-offset: 4px;
}
.search-box button {
min-width: 44px;
min-height: 44px;
padding-inline: 10px;
border: 0;
background: transparent;
font-size: 12px;
@ -1255,11 +1227,12 @@ fieldset {
}
.map-pin {
width: 46px;
height: 46px;
width: 52px;
height: 52px;
position: absolute;
display: grid;
place-items: center;
padding-inline: 8px;
border: 3px solid white;
border-radius: 50%;
background: var(--ink);
@ -1269,8 +1242,8 @@ fieldset {
.map-pin span {
position: absolute;
top: 50px;
padding: 4px 7px;
top: 56px;
padding: 5px 9px;
background: white;
color: var(--ink);
font-size: 10px;
@ -1779,6 +1752,69 @@ fieldset {
}
}
@media (min-width: 821px) {
.hero {
grid-template-rows: minmax(0, 1fr) auto;
align-items: stretch;
}
.hero-copy {
grid-column: 1;
grid-row: 1 / span 2;
}
.hero-media {
grid-column: 2;
grid-row: 1;
min-height: 0;
}
.hero-order {
position: static;
grid-column: 2;
grid-row: 2;
width: auto;
min-height: 8.75rem;
margin: 0;
box-shadow: none;
}
}
.builder-form legend,
.field-label,
.select-grid label {
line-height: 1.45;
}
.choice-row label > span small,
.menu-foot small {
font-variant-numeric: tabular-nums;
}
@media (max-width: 820px) {
.hero-order {
position: static;
width: auto;
margin: 0;
box-shadow: none;
}
}
@media (max-width: 620px) {
.hero-order {
display: grid;
}
.service-row {
grid-template-columns: minmax(0, 1fr);
}
.service-row p {
min-width: 0;
overflow-wrap: anywhere;
}
}
@media print {
.truth-bar,
.site-head,
@ -1805,3 +1841,107 @@ fieldset {
border: 1px solid #777;
}
}
/* ── 2026-08-30: 초광폭은 확장이 아니라 독립된 구성 상태다 ─────────── */
@media (min-width: 120rem) {
.site-head .wrap,
.service-strip .wrap,
.section > .wrap,
.site-foot .wrap {
max-width: var(--workbench-wide);
}
.hero {
width: min(100%, var(--hero-wide));
min-height: clamp(43rem, 48vw, 54rem);
grid-template-columns: minmax(0, 1fr) minmax(42rem, 1.14fr);
margin-inline: auto;
border-inline: 1px solid var(--line);
}
.hero-copy {
width: min(100%, var(--copy-wide));
max-width: none;
justify-self: end;
padding: clamp(5.5rem, 6vw, 8rem) clamp(4rem, 5vw, 6rem);
}
.hero h1 {
max-width: none;
font-size: clamp(4.5rem, 3.6rem + 1.5vw, 5.25rem);
white-space: nowrap;
}
.hero h1 br {
display: none;
}
.hero-wide-space {
display: inline;
}
.hero-lead {
max-width: 38rem;
}
.hero-media {
width: min(100%, var(--media-wide));
min-height: clamp(43rem, 48vw, 54rem);
justify-self: end;
}
.hero-media img {
object-position: 52% center;
}
.hero-order {
right: clamp(1.5rem, 3vw, 3.5rem);
width: min(21rem, calc(100% - 3rem));
}
.menu-grid,
.builder-layout,
.store-layout {
max-width: var(--workbench-wide);
margin-inline: auto;
}
.builder-layout {
grid-template-columns: minmax(0, 1fr) minmax(27rem, 34rem);
}
.process-grid,
.franchise-callout {
width: min(100%, var(--stage-wide));
margin-inline: auto;
border-inline: 1px solid var(--line);
}
.process-grid {
/* .wrap's content cap must not silently win over this named wide stage. */
grid-template-columns: minmax(0, 0.98fr) minmax(0, 1.02fr);
}
.process-section > .process-grid {
max-width: var(--stage-wide);
}
.process-copy {
/* Reset both inherited viewport-derived side paddings: leaving the left
value in place leaves a 12px text column at a 2560px viewport. */
width: 100%;
max-width: none;
justify-self: stretch;
padding-inline: clamp(4rem, 5vw, 6rem);
}
.process-media,
.franchise-image {
min-height: clamp(42rem, 43vw, 52rem);
}
.franchise-copy {
max-width: 43rem;
padding-inline: clamp(4rem, 5vw, 6rem);
}
}

View file

@ -9,6 +9,7 @@
--line-strong: #8a8379;
--blue: #2147e7;
--blue-dark: #1533ac;
--blue-soft: #8ea5ff;
--blue-pale: #dce4ff;
--steel: #d9dcda;
--cheese: #ffc642;
@ -18,6 +19,12 @@
--font-sans: "Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-mono: "IBM Plex Mono", "SFMono-Regular", Consolas, monospace;
--wrap: 1180px;
/* 초광폭에서도 히어로와 후속 정보 표면은 같은 폭을 쓰지 않는다. */
--hero-wide: 2160px;
--stage-wide: 1680px;
--workbench-wide: 1480px;
--copy-wide: 900px;
--media-wide: 1120px;
--gutter: clamp(18px, 4vw, 52px);
--section: clamp(76px, 10vw, 144px);
--space-1: 4px;
@ -41,3 +48,15 @@
--section: 72px;
}
}
@media (min-width: 120rem) {
:root {
--wrap: 1320px;
}
}
@media (min-width: 150rem) {
:root {
--wrap: 1400px;
}
}

View file

@ -4,28 +4,28 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#f3f4f3">
<meta name="description" content="가상 공인중개 쇼케이스의 매물 사실, 표시 항목, AI 분석 근거, 변경 이력, 게시 가능 여부를 한 화면에서 검수하는 상세 관리자 운영 데모입니다.">
<meta name="description" content="예시 매물의 표시 항목, 확인 근거와 변경 기록을 한 화면에서 살펴보고 공개 전 확인할 순서를 정리하는 운영 안내입니다.">
<meta name="robots" content="noindex, nofollow">
<meta property="og:title" content="한남자리 관리 데모 — 매물 검수">
<meta property="og:description" content="예시 매물의 표시 항목, AI 근거와 게시 방어선을 살피는 운영 화면">
<meta property="og:title" content="한남자리 운영 안내 — 매물 확인">
<meta property="og:description" content="예시 매물의 표시 항목과 확인 근거를 살피는 운영 화면">
<meta property="og:image" content="../og.png">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="한남자리 관리 데모 — 매물 검수">
<meta name="twitter:title" content="한남자리 운영 안내 — 매물 확인">
<meta name="twitter:image" content="../og.png">
<title>한남자리 관리 데모 — 매물 검수</title>
<title>한남자리 운영 안내 — 매물 확인</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='14' fill='%23141413'/%3E%3Ctext x='32' y='39' text-anchor='middle' font-family='Arial' font-size='19' font-weight='700' fill='%23c6a15b'%3EHJ%3C/text%3E%3C/svg%3E">
<link rel="stylesheet" href="../styles/tokens.css?v=20260829c">
<link rel="stylesheet" href="../styles/admin.css?v=20260829c">
<script src="../scripts/admin.js?v=20260829c" defer></script>
<link rel="stylesheet" href="../styles/tokens.css?v=20260912all">
<link rel="stylesheet" href="../styles/admin.css?v=20260912all">
<script src="../scripts/admin.js?v=20260830e" defer></script>
</head>
<body class="admin-body">
<a class="skip-link" href="#admin-main">본문 바로가기</a>
<div class="demo-strip" role="region" aria-label="쇼케이스 고지">쇼케이스 · 가상 브랜드 · 모든 사무소, 매물, 고객과 기록은 합성 예시입니다</div>
<div class="demo-strip" role="region" aria-label="예시 자료 안내">안내 · 예시 자료 · 사무소, 매물, 고객과 기록은 실제 정보가 아니며 광고·연락·저장은 연결되지 않습니다</div>
<div class="admin-shell">
<aside class="admin-sidebar" data-sidebar aria-label="관리 메뉴">
<div class="sidebar-brand-shell">
<a href="../" aria-label="한남자리 공개 쇼케이스로 이동"><span class="sidebar-mark">HJ</span><span><strong class="sidebar-brand">한남자리</strong><small>운영 화면 데모</small></span></a>
<a href="../" aria-label="한남자리 공개 안내로 이동"><span class="sidebar-mark">HJ</span><span><strong class="sidebar-brand">한남자리</strong><small>매물 확인 안내</small></span></a>
<button type="button" class="sidebar-close" data-close-sidebar aria-label="관리 메뉴 닫기">×</button>
</div>
@ -45,9 +45,9 @@
</nav>
<div class="sidebar-foot">
<span class="status-badge status-demo">데모 환경</span>
<span class="status-badge status-demo">예시 자료</span>
<p>실제 광고 게시와 외부 연락은 연결되지 않습니다.</p>
<a href="../">공개 화면 보기 ↗</a>
<a href="../">공개 안내 보기 ↗</a>
</div>
</aside>
@ -55,7 +55,7 @@
<main class="admin-main" id="admin-main">
<header class="admin-topbar">
<button type="button" class="mobile-sidebar-button" aria-expanded="false" data-open-sidebar><span aria-hidden="true">☰</span><span class="sr-only">관리 메뉴 열기</span></button>
<button type="button" class="mobile-sidebar-button" aria-label="관리 메뉴 열기" aria-expanded="false" data-open-sidebar><span aria-hidden="true">☰</span></button>
<div class="breadcrumb"><span>운영</span><span aria-hidden="true">/</span><strong>매물 검수</strong></div>
<div class="topbar-actions">
<button type="button" class="icon-button" aria-label="명령 메뉴 열기" data-command-button><span aria-hidden="true">⌕</span><kbd>Ctrl K</kbd></button>
@ -67,13 +67,13 @@
<div class="admin-content">
<header class="page-heading">
<div>
<div class="heading-badges"><span class="status-badge status-demo">쇼케이스 · 가상 브랜드</span><span>자료 기준 2026.08.29</span></div>
<div class="heading-badges"><span class="status-badge status-demo">예시 자료 · 가상 브랜드</span><span>자료 기준 2026.08.29</span></div>
<h1>매물 검수 대기열</h1>
<p>게시 전에 매물 사실, 표시 항목, AI 근거와 변경 이력을 한 자리에서 확인합니다.</p>
</div>
<div class="heading-actions">
<button type="button" class="button button-ghost" data-open-preview>쇼케이스 미리보기</button>
<button type="button" class="button button-primary" data-new-draft data-inspector-trigger>새 예시 초안</button>
<button type="button" class="button button-ghost" data-open-preview>공개 안내 보기</button>
<button type="button" class="button button-primary" data-new-draft data-inspector-trigger>새 검토 항목</button>
</div>
</header>
@ -84,48 +84,48 @@
<section class="queue-toolbar" aria-label="대기열 도구">
<label class="search-field"><span class="sr-only">매물 검색</span><span aria-hidden="true">⌕</span><input type="search" placeholder="매물명, 거래 형태, 담당 검색" data-queue-search></label>
<label><span class="sr-only">상태 필터</span><select data-status-filter><option value="all">모든 상태</option><option value="review">검수 필요</option><option value="visible">쇼케이스 공개</option><option value="paused">보류</option><option value="contracted">계약 완료</option><option value="draft">초안</option></select></label>
<label><span class="sr-only">상태 필터</span><select data-status-filter><option value="all">모든 상태</option><option value="review">검수 필요</option><option value="visible">예시 공개</option><option value="paused">보류</option><option value="contracted">계약 완료</option><option value="draft">초안</option></select></label>
<label><span class="sr-only">정렬 기준</span><select data-sort-select><option value="updated-desc">최근 수정순</option><option value="updated-asc">오래된 수정순</option><option value="title-asc">매물명순</option><option value="status-asc">상태순</option></select></label>
<label class="state-simulator"><span>상태 시연</span><select data-state-simulator><option value="normal">정상</option><option value="loading">불러오는 중</option><option value="empty">빈 대기열</option><option value="error">불러오기 실패</option><option value="save-error">저장 실패</option><option value="stale">자료 오래됨</option><option value="conflict">버전 충돌</option><option value="permission">권한 제한</option></select></label>
<select data-state-simulator hidden aria-hidden="true"><option value="normal">정상</option><option value="loading">불러오는 중</option><option value="empty">빈 대기열</option><option value="error">불러오기 실패</option><option value="save-error">저장 실패</option><option value="stale">자료 오래됨</option><option value="conflict">버전 충돌</option><option value="permission">권한 제한</option></select>
</section>
<div class="admin-workbench">
<section class="queue-panel" id="queue" aria-labelledby="queue-title" aria-busy="false" data-queue-panel>
<header class="panel-heading">
<div><h2 id="queue-title">현재 대기열</h2><span data-result-count>6개 예시</span></div>
<div><h2 id="queue-title">현재 대기열</h2><span data-result-count>6건</span></div>
<div><span class="queue-legend"><i class="legend-block" aria-hidden="true"></i>차단</span><span class="queue-legend"><i class="legend-warn" aria-hidden="true"></i>확인</span></div>
</header>
<div class="table-scroll">
<table class="queue-table">
<caption class="sr-only">쇼케이스용 예시 매물 검수 대기열</caption>
<caption class="sr-only">예시 매물 확인 대기열</caption>
<thead>
<tr><th scope="col">매물</th><th scope="col">거래</th><th scope="col">예시 조건</th><th scope="col">마지막 확인</th><th scope="col">표시 검수</th><th scope="col">AI 검토</th><th scope="col">담당</th></tr>
</thead>
<tbody data-queue-body>
<tr class="is-selected" data-queue-row data-id="DEMO-HJ-101" data-status="review" data-updated="2026-08-29T10:42:00+09:00" data-title="한남 생활권 정원 주거" aria-selected="true">
<td class="row-cell-select"><button type="button" class="row-select-overlay" data-select-row aria-label="한남 생활권 정원 주거 조사관 열기"></button><span class="row-title" aria-hidden="true"><span class="row-thumb"><img src="../assets/hannam-evening-640.webp" width="80" height="54" alt="서울 언덕 생활권의 주거 외관을 표현한 연출 이미지"></span><span><strong>한남 생활권 정원 주거</strong><small>DEMO-HJ-101 · 예시 매물</small></span></span></td>
<td><span>매매</span><small>연립주택</small></td><td><strong>38억 원</strong><small>전용 118.42㎡</small></td><td><span>2026.08.27</span><small>2일 전 · 모의</small></td><td><span class="cell-status is-blocked"><i aria-hidden="true"></i>2개 차단</span></td><td><span class="cell-status is-warn"><i aria-hidden="true"></i>검토 전</span></td><td><span class="assignee"><i>김</i>김유진</span></td>
<td class="row-cell-select"><button type="button" class="row-select-overlay" data-select-row aria-label="한남 생활권 정원 주거 상세 정보 열기"></button><span class="row-title" aria-hidden="true"><span class="row-thumb"><img src="../assets/hannam-evening-640.webp" width="80" height="54" alt="서울 언덕 생활권의 주거 외관을 표현한 연출 이미지"></span><span><strong>한남 생활권 정원 주거</strong><small>자료번호 101 · 예시 매물</small></span></span></td>
<td><span>매매</span><small>연립주택</small></td><td><strong>38억 원</strong><small>전용 118.42㎡</small></td><td><span>2026.08.27</span><small>2일 전 · 예시</small></td><td><span class="cell-status is-blocked"><i aria-hidden="true"></i>2개 확인 필요</span></td><td><span class="cell-status is-warn"><i aria-hidden="true"></i>검토 전</span></td><td><span class="assignee"><i>김</i>김유진</span></td>
</tr>
<tr data-queue-row data-id="DEMO-HJ-102" data-status="visible" data-updated="2026-08-29T09:18:00+09:00" data-title="한강 가까운 복층 주거" aria-selected="false">
<td class="row-cell-select"><button type="button" class="row-select-overlay" data-select-row aria-label="한강 가까운 복층 주거 조사관 열기"></button><span class="row-title" aria-hidden="true"><span class="row-thumb row-thumb-plan"><i></i></span><span><strong>한강 가까운 복층 주거</strong><small>DEMO-HJ-102 · 예시 매물</small></span></span></td>
<td><span>전세</span><small>공동주택</small></td><td><strong>전세 22억</strong><small>전용 142.80㎡</small></td><td><span>2026.08.26</span><small>3일 전 · 모의</small></td><td><span class="cell-status is-pass"><i aria-hidden="true"></i>데모 통과</span></td><td><span class="cell-status is-pass"><i aria-hidden="true"></i>수정 승인</span></td><td><span class="assignee"><i>박</i>박소연</span></td>
<td class="row-cell-select"><button type="button" class="row-select-overlay" data-select-row aria-label="한강 가까운 복층 주거 상세 정보 열기"></button><span class="row-title" aria-hidden="true"><span class="row-thumb row-thumb-plan"><i></i></span><span><strong>한강 가까운 복층 주거</strong><small>자료번호 102 · 예시 매물</small></span></span></td>
<td><span>전세</span><small>공동주택</small></td><td><strong>전세 22억</strong><small>전용 142.80㎡</small></td><td><span>2026.08.26</span><small>3일 전 · 예시</small></td><td><span class="cell-status is-pass"><i aria-hidden="true"></i>안내 확인</span></td><td><span class="cell-status is-pass"><i aria-hidden="true"></i>수정 확인</span></td><td><span class="assignee"><i>박</i>박소연</span></td>
</tr>
<tr data-queue-row data-id="DEMO-HJ-103" data-status="paused" data-updated="2026-08-28T17:25:00+09:00" data-title="조용한 골목의 테라스 주거" aria-selected="false">
<td class="row-cell-select"><button type="button" class="row-select-overlay" data-select-row aria-label="조용한 골목의 테라스 주거 조사관 열기"></button><span class="row-title" aria-hidden="true"><span class="row-thumb row-thumb-stale"><i></i></span><span><strong>조용한 골목의 테라스 주거</strong><small>DEMO-HJ-103 · 예시 매물</small></span></span></td>
<td class="row-cell-select"><button type="button" class="row-select-overlay" data-select-row aria-label="조용한 골목의 테라스 주거 상세 정보 열기"></button><span class="row-title" aria-hidden="true"><span class="row-thumb row-thumb-stale"><i></i></span><span><strong>조용한 골목의 테라스 주거</strong><small>자료번호 103 · 예시 매물</small></span></span></td>
<td><span>월세</span><small>다세대주택</small></td><td><strong>5억 · 월 1,200만</strong><small>전용 140.84㎡</small></td><td><span>2026.08.11</span><small>18일 전 · 오래됨</small></td><td><span class="cell-status is-warn"><i aria-hidden="true"></i>다시 확인</span></td><td><span class="cell-status is-stale"><i aria-hidden="true"></i>오래됨</span></td><td><span class="assignee"><i>김</i>김유진</span></td>
</tr>
<tr data-queue-row data-id="DEMO-HJ-104" data-status="contracted" data-updated="2026-08-28T12:40:00+09:00" data-title="언덕 위 작은 정원 주거" aria-selected="false">
<td class="row-cell-select"><button type="button" class="row-select-overlay" data-select-row aria-label="언덕 위 작은 정원 주거 조사관 열기"></button><span class="row-title" aria-hidden="true"><span class="row-thumb row-thumb-closed"><i></i></span><span><strong>언덕 위 작은 정원 주거</strong><small>DEMO-HJ-104 · 예시 매물</small></span></span></td>
<td><span>매매</span><small>단독주택</small></td><td><strong>상담 종료</strong><small>공개 목록 제외</small></td><td><span>2026.08.28</span><small>1일 전 · 모의</small></td><td><span class="cell-status is-muted"><i aria-hidden="true"></i>게시 안 함</span></td><td><span class="cell-status is-muted"><i aria-hidden="true"></i>종료</span></td><td><span class="assignee"><i>박</i>박소연</span></td>
<td class="row-cell-select"><button type="button" class="row-select-overlay" data-select-row aria-label="언덕 위 작은 정원 주거 상세 정보 열기"></button><span class="row-title" aria-hidden="true"><span class="row-thumb row-thumb-closed"><i></i></span><span><strong>언덕 위 작은 정원 주거</strong><small>자료번호 104 · 예시 매물</small></span></span></td>
<td><span>매매</span><small>단독주택</small></td><td><strong>상담 종료</strong><small>공개 목록 제외</small></td><td><span>2026.08.28</span><small>1일 전 · 예시</small></td><td><span class="cell-status is-muted"><i aria-hidden="true"></i>공개 안 함</span></td><td><span class="cell-status is-muted"><i aria-hidden="true"></i>종료</span></td><td><span class="assignee"><i>박</i>박소연</span></td>
</tr>
<tr data-queue-row data-id="DEMO-HJ-105" data-status="draft" data-updated="2026-08-27T15:05:00+09:00" data-title="남산 가까운 가족 주거" aria-selected="false">
<td class="row-cell-select"><button type="button" class="row-select-overlay" data-select-row aria-label="남산 가까운 가족 주거 조사관 열기"></button><span class="row-title" aria-hidden="true"><span class="row-thumb row-thumb-draft"><i></i></span><span><strong>남산 가까운 가족 주거</strong><small>DEMO-HJ-105 · 예시 매물</small></span></span></td>
<td><span>전세</span><small>공동주택</small></td><td><strong>조건 작성 중</strong><small>면적 입력됨</small></td><td><span>확인 전</span><small>모의 서류 없음</small></td><td><span class="cell-status is-blocked"><i aria-hidden="true"></i>5개 차단</span></td><td><span class="cell-status is-muted"><i aria-hidden="true"></i>실행 전</span></td><td><span class="assignee"><i>최</i>최민서</span></td>
<td class="row-cell-select"><button type="button" class="row-select-overlay" data-select-row aria-label="남산 가까운 가족 주거 상세 정보 열기"></button><span class="row-title" aria-hidden="true"><span class="row-thumb row-thumb-draft"><i></i></span><span><strong>남산 가까운 가족 주거</strong><small>자료번호 105 · 예시 매물</small></span></span></td>
<td><span>전세</span><small>공동주택</small></td><td><strong>조건 작성 중</strong><small>면적 입력됨</small></td><td><span>확인 전</span><small>참고 자료 없음</small></td><td><span class="cell-status is-blocked"><i aria-hidden="true"></i>5개 확인 필요</span></td><td><span class="cell-status is-muted"><i aria-hidden="true"></i>분석 전</span></td><td><span class="assignee"><i>최</i>최민서</span></td>
</tr>
<tr data-queue-row data-id="DEMO-HJ-106" data-status="review" data-updated="2026-08-26T11:20:00+09:00" data-title="독서당길 안쪽 복층 주거" aria-selected="false">
<td class="row-cell-select"><button type="button" class="row-select-overlay" data-select-row aria-label="독서당길 안쪽 복층 주거 조사관 열기"></button><span class="row-title" aria-hidden="true"><span class="row-thumb row-thumb-review"><i></i></span><span><strong>독서당길 안쪽 복층 주거</strong><small>DEMO-HJ-106 · 예시 매물</small></span></span></td>
<td><span>매매</span><small>공동주택</small></td><td><strong>41억 원</strong><small>전용 151.20㎡</small></td><td><span>2026.08.25</span><small>4일 전 · 모의</small></td><td><span class="cell-status is-warn"><i aria-hidden="true"></i>동의 대기</span></td><td><span class="cell-status is-blocked"><i aria-hidden="true"></i>실행 실패</span></td><td><span class="assignee"><i>최</i>최민서</span></td>
<td class="row-cell-select"><button type="button" class="row-select-overlay" data-select-row aria-label="독서당길 안쪽 복층 주거 상세 정보 열기"></button><span class="row-title" aria-hidden="true"><span class="row-thumb row-thumb-review"><i></i></span><span><strong>독서당길 안쪽 복층 주거</strong><small>자료번호 106 · 예시 매물</small></span></span></td>
<td><span>매매</span><small>공동주택</small></td><td><strong>41억 원</strong><small>전용 151.20㎡</small></td><td><span>2026.08.25</span><small>4일 전 · 예시</small></td><td><span class="cell-status is-warn"><i aria-hidden="true"></i>동의 대기</span></td><td><span class="cell-status is-blocked"><i aria-hidden="true"></i>분석 확인 필요</span></td><td><span class="assignee"><i>최</i>최민서</span></td>
</tr>
</tbody>
</table>
@ -135,7 +135,7 @@
<span></span><span></span><span></span><p>대기열을 불러오는 중입니다.</p>
</div>
<div class="queue-empty" hidden data-queue-empty>
<span aria-hidden="true">○</span><strong data-empty-title>현재 조건에 맞는 예시가 없습니다.</strong><p data-empty-copy>필터를 초기화하면 전체 대기열을 다시 볼 수 있습니다.</p><button type="button" class="button button-ghost" data-reset-filter>필터 초기화</button>
<span aria-hidden="true">○</span><strong data-empty-title>현재 조건에 맞는 항목이 없습니다.</strong><p data-empty-copy>필터를 초기화하면 전체 대기열을 다시 볼 수 있습니다.</p><button type="button" class="button button-ghost" data-reset-filter>필터 초기화</button>
</div>
</section>
@ -143,8 +143,8 @@
<aside class="inspector is-open" data-inspector aria-labelledby="inspector-title">
<header class="inspector-header">
<div><span class="status-badge status-warn" data-inspector-status>검수 필요</span><small data-inspector-id>DEMO-HJ-101 · 예시 매물</small><h2 id="inspector-title" data-inspector-title>한남 생활권 정원 주거</h2></div>
<button type="button" class="inspector-close" data-close-inspector aria-label="조사관과 선택 행 닫기">×</button>
<div><span class="status-badge status-warn" data-inspector-status>검수 필요</span><small data-inspector-id>자료번호 101 · 예시 매물</small><h2 id="inspector-title" data-inspector-title>한남 생활권 정원 주거</h2></div>
<button type="button" class="inspector-close" data-close-inspector aria-label="선택한 매물 정보 닫기">×</button>
</header>
<div class="inspector-media">
@ -153,7 +153,7 @@
</div>
<div class="channel-gates" id="office-gate">
<button type="button" class="channel-gate is-pass" data-open-preview><span><i aria-hidden="true">✓</i><strong>쇼케이스</strong></span><small>미리보기 가능</small></button>
<button type="button" class="channel-gate is-pass" data-open-preview><span><i aria-hidden="true">✓</i><strong>공개 안내</strong></span><small>예시 자료 보기</small></button>
<button type="button" class="channel-gate is-blocked" data-block-publish><span><i aria-hidden="true">!</i><strong>실제 광고</strong></span><small>영구 차단 · 7개 이유</small></button>
</div>
@ -168,45 +168,45 @@
<div class="inspector-scroll">
<section class="tab-panel" role="tabpanel" id="inspector-basic" aria-labelledby="tab-basic" data-panel="basic">
<div class="panel-section-heading"><div><span>기본 정보</span><h3>예시 거래 조건</h3></div><button type="button" class="text-button" data-simulate-save>수정 시연</button></div>
<div class="panel-section-heading"><div><span>기본 정보</span><h3>표시된 거래 조건</h3></div><button type="button" class="text-button" data-simulate-save>변경 내용 보기</button></div>
<dl class="data-grid" data-basic-grid>
<div><dt>거래 형태</dt><dd>매매</dd></div><div><dt>예시 가격</dt><dd>38억 원</dd></div><div><dt>유형</dt><dd>연립주택</dd></div><div><dt>전용 면적</dt><dd>118.42㎡ · 35.8평</dd></div><div><dt>방과 욕실</dt><dd>방 3 · 욕실 2</dd></div><div><dt>입주 가능</dt><dd>2026.12.20 이후 협의</dd></div>
<div><dt>거래 형태</dt><dd>매매</dd></div><div><dt>표시 가격</dt><dd>38억 원</dd></div><div><dt>유형</dt><dd>연립주택</dd></div><div><dt>전용 면적</dt><dd>118.42㎡ · 35.8평</dd></div><div><dt>방과 욕실</dt><dd>방 3 · 욕실 2</dd></div><div><dt>입주 가능</dt><dd>2026.12.20 이후 협의</dd></div>
</dl>
<div class="inline-notice"><span aria-hidden="true">i</span><p><strong>가상 데이터</strong> 가격과 면적은 쇼케이스 동작 검증용이며 실제 거래 조건이 아닙니다.</p></div>
<div class="inline-notice"><span aria-hidden="true">i</span><p><strong>예시 자료</strong> 가격과 면적은 안내를 위한 값이며 실제 거래 조건이 아닙니다.</p></div>
<div class="panel-section-heading"><div><span>담당과 상태</span><h3>현재 업무 흐름</h3></div></div>
<ol class="state-path">
<li class="is-done"><span></span><div><strong>초안</strong><small>2026.08.27 · 예시</small></div></li><li class="is-done"><span></span><div><strong>접수 확인</strong><small>2026.08.28 · 예시</small></div></li><li class="is-current"><span></span><div><strong>검수 필요</strong><small>현재 · 2개 차단</small></div></li><li><span></span><div><strong>게시 준비</strong><small>차단 해제 후</small></div></li>
<li class="is-done"><span></span><div><strong>초안</strong><small>2026.08.27 · 예시</small></div></li><li class="is-done"><span></span><div><strong>접수 확인</strong><small>2026.08.28 · 예시</small></div></li><li class="is-current"><span></span><div><strong>검수 필요</strong><small>현재 · 2개 확인 필요</small></div></li><li><span></span><div><strong>게시 준비</strong><small>확인 후</small></div></li>
</ol>
</section>
<section class="tab-panel" role="tabpanel" id="inspector-legal" aria-labelledby="tab-legal" data-panel="legal" hidden>
<div class="panel-section-heading"><div><span>이중 게시 게이트</span><h3>채널마다 다른 통과 조건</h3></div><button type="button" class="text-button" data-focus-missing>첫 누락으로 이동</button></div>
<div class="panel-section-heading"><div><span>공개 전 확인</span><h3>용도마다 다른 확인 항목</h3></div><button type="button" class="text-button" data-focus-missing>확인 필요 항목으로 이동</button></div>
<article class="gate-detail is-pass">
<header><span aria-hidden="true">✓</span><div><strong>쇼케이스 미리보기</strong><small>6 / 6 통과</small></div></header>
<ul><li>상시 데모 표지</li><li>개별 예시 매물 배지</li><li>생성 이미지 출처</li><li>실제 연락 기능 비활성</li><li>정확한 주소와 등록번호 없음</li><li>AI 데모와 사람 검토 전 표시</li></ul>
<header><span aria-hidden="true">✓</span><div><strong>예시 자료 안내</strong><small>6개 항목 확인</small></div></header>
<ul><li>예시 자료 안내</li><li>개별 예시 매물 표기</li><li>생성 이미지 출처</li><li>실제 연락 기능 없음</li><li>정확한 주소와 등록번호 없음</li><li>AI 결과는 참고용이며 사람 확인 전임</li></ul>
</article>
<article class="gate-detail is-blocked" id="real-ad-gate">
<header><span aria-hidden="true">!</span><div><strong>실제 광고</strong><small>영구 차단 · 가상 브랜드</small></div></header>
<p>실제 사무소 등록정보와 중개의뢰 관계가 없으므로 이 데모에서 통과시킬 수 없습니다.</p>
<header><span aria-hidden="true">!</span><div><strong>실제 광고</strong><small>영구 차단 · 예시 자료</small></div></header>
<p>실제 사무소 등록정보와 중개의뢰 관계가 없어 광고에 필요한 확인을 마칠 수 없습니다.</p>
<button type="button" class="button button-ghost button-small" data-block-publish>차단 이유 7개 보기</button>
</article>
<div class="check-list" data-legal-checks>
<div class="check-row is-pass"><span aria-hidden="true">✓</span><div><strong>소재지 · 면적 · 가격 · 유형 · 거래 형태</strong><small>쇼케이스 fixture에 구조화됨</small></div><button type="button">보기</button></div>
<div class="check-row is-pass"><span aria-hidden="true">✓</span><div><strong>총층수 · 사용승인일 · 방 · 욕실</strong><small>모의 서류 기준 · 실제 증빙 아님</small></div><button type="button">보기</button></div>
<div class="check-row is-pass"><span aria-hidden="true">✓</span><div><strong>소재지 · 면적 · 가격 · 유형 · 거래 형태</strong><small>예시 자료에 구조화해 표시</small></div><button type="button">보기</button></div>
<div class="check-row is-pass"><span aria-hidden="true">✓</span><div><strong>총층수 · 사용승인일 · 방 · 욕실</strong><small>안내용 자료 기준 · 실제 증빙 아님</small></div><button type="button">보기</button></div>
<div class="check-row is-blocked" id="missing-orientation" tabindex="-1"><span aria-hidden="true">!</span><div><strong>방향 판단 근거 미확인</strong><small>남동향 값은 있으나 현장 또는 실제 서류 근거 없음</small></div><button type="button" data-jump-field="missing-orientation">수정 위치</button></div>
<div class="check-row is-blocked"><span aria-hidden="true">!</span><div><strong>실제 중개의뢰 관계 없음</strong><small>가상 매물이므로 실제 광고 불가</small></div><button type="button" data-block-publish>이유</button></div>
<div class="check-row is-warn"><span aria-hidden="true">·</span><div><strong>관리비 포함·별도 항목</strong><small>모의 기준월과 항목을 표시함</small></div><button type="button">보기</button></div>
<div class="check-row is-blocked"><span aria-hidden="true">!</span><div><strong>실제 중개의뢰 관계 없음</strong><small>예시 매물이므로 실제 광고 불가</small></div><button type="button" data-block-publish>이유</button></div>
<div class="check-row is-warn"><span aria-hidden="true">·</span><div><strong>관리비 포함·별도 항목</strong><small>표시 기준월과 항목을 안내함</small></div><button type="button">보기</button></div>
</div>
</section>
<section class="tab-panel" role="tabpanel" id="inspector-docs" aria-labelledby="tab-docs" data-panel="docs" hidden>
<div class="panel-section-heading"><div><span>모의 서류</span><h3>증빙과 예시를 구분</h3></div></div>
<div class="panel-section-heading"><div><span>참고 자료</span><h3>예시 정보와 확인 자료를 구분</h3></div></div>
<div class="document-list">
<article><span class="doc-icon" aria-hidden="true">▧</span><div><strong>건축물대장 형식 예시</strong><small>2026.08.27 추가 · 실제 공문서 아님</small></div><span class="status-badge status-demo">모의</span></article>
<article><span class="doc-icon" aria-hidden="true">▧</span><div><strong>의뢰 조건 메모</strong><small>2026.08.27 추가 · 가상 입력</small></div><span class="status-badge status-demo">모의</span></article>
<article><span class="doc-icon" aria-hidden="true">▧</span><div><strong>건축물대장 형식 안내</strong><small>2026.08.27 추가 · 실제 공문서 아님</small></div><span class="status-badge status-demo">안내용</span></article>
<article><span class="doc-icon" aria-hidden="true">▧</span><div><strong>의뢰 조건 메모</strong><small>2026.08.27 추가 · 예시 입력</small></div><span class="status-badge status-demo">안내용</span></article>
<article class="is-missing"><span class="doc-icon" aria-hidden="true">×</span><div><strong>실제 등기사항</strong><small>실주소와 실제 권리 자료를 연결하지 않음</small></div><span class="status-badge status-blocked">없음</span></article>
<article class="is-missing"><span class="doc-icon" aria-hidden="true">×</span><div><strong>현장 확인 기록</strong><small>소음, 일조, 누수와 마감 상태 확인 안 됨</small></div><span class="status-badge status-warn">대기</span></article>
</div>
@ -214,65 +214,65 @@
</section>
<section class="tab-panel" role="tabpanel" id="inspector-ai" aria-labelledby="tab-ai" data-panel="ai" hidden>
<div class="panel-section-heading"><div><span>조건 비교 규칙 demo-0.3</span><h3>AI 참고 분석 데모</h3></div><span class="status-badge status-warn">사람 검토 전</span></div>
<dl class="run-meta"><div><dt>실행 번호</dt><dd>AIRUN-DEMO-0042</dd></div><div><dt>입력 버전</dt><dd>고객 3 · 매물 7</dd></div><div><dt>실행 방식</dt><dd>결정적 데모</dd></div><div><dt>공개 가능</dt><dd>아니요</dd></div></dl>
<div class="panel-section-heading"><div><span>조건 비교</span><h3>AI 참고 결과</h3></div><span class="status-badge status-warn">사람 검토 전</span></div>
<dl class="run-meta"><div><dt>분석 기록</dt><dd>0042</dd></div><div><dt>입력 기준</dt><dd>고객 3 · 매물 7</dd></div><div><dt>결과 성격</dt><dd>안내용 비교</dd></div><div><dt>실제 광고</dt><dd>불가</dd></div></dl>
<div class="ai-summary"><span>현재 판단</span><strong>조건부 적합</strong><div><small>필수 충족 4 / 5</small><small>충돌 1</small><small>확인 필요 2</small></div></div>
<div class="claim-review">
<article class="is-fact"><header><span>사실</span><strong>예산 상한보다 2억 원 낮음</strong></header><p>가상 의뢰인 가격 입력 · 2026.08.27</p><footer><span>근거 강도 높음</span><button type="button" data-claim-action="approve">확인</button></footer></article>
<article class="is-fact"><header><span>사실</span><strong>전용면적이 희망 범위 안</strong></header><p>쇼케이스용 모의 서류 · 실제 공문서 아님</p><footer><span>근거 강도 높음</span><button type="button" data-claim-action="approve">확인</button></footer></article>
<article class="is-fact"><header><span>표시값</span><strong>예산 상한보다 2억 원 낮음</strong></header><p>예시 의뢰 조건 · 2026.08.27</p><footer><span>표시 자료 기준</span><button type="button" data-claim-action="approve">확인</button></footer></article>
<article class="is-fact"><header><span>표시값</span><strong>전용면적이 희망 범위 안</strong></header><p>안내용 자료 · 실제 공문서 아님</p><footer><span>표시 자료 기준</span><button type="button" data-claim-action="approve">확인</button></footer></article>
<article class="is-unknown"><header><span>확인 불가</span><strong>광화문 이동 시간</strong></header><p>현재 교통 자료가 연결되지 않아 추정하지 않음</p><footer><span>빈칸 유지</span><button type="button" data-claim-action="keep">유지</button></footer></article>
<article class="is-edited"><header><span>수정 필요</span><strong><s>조용한 주거 환경입니다.</s></strong></header><p>현장 근거가 없어 “소음 수준은 현장 확인이 필요합니다”로 바꿉니다.</p><footer><span>지원되지 않은 표현</span><button type="button" data-claim-action="edit">수정 승인</button></footer></article>
<article class="is-edited"><header><span>수정 필요</span><strong><s>조용한 주거 환경입니다.</s></strong></header><p>현장 근거가 없어 “소음 수준은 현장 확인이 필요합니다”로 안내합니다.</p><footer><span>확인 근거 없음</span><button type="button" data-claim-action="edit">수정 반영</button></footer></article>
</div>
<button type="button" class="button button-primary inspector-wide-action" data-review-ai>사람 검토 완료 시연</button>
<button type="button" class="button button-primary inspector-wide-action" data-review-ai>검토 결과 보기</button>
</section>
<section class="tab-panel" role="tabpanel" id="inspector-inquiries" aria-labelledby="tab-inquiries" data-panel="inquiries" hidden>
<div class="panel-section-heading"><div><span>고객 조건 3판</span><h3>김○○ · 예시 고객</h3></div><span class="status-badge status-demo">연락처 마스킹</span></div>
<dl class="data-grid customer-grid"><div><dt>거래 형태</dt><dd>매매</dd></div><div><dt>예산 상한</dt><dd>40억 원 · 예시</dd></div><div><dt>전용 면적</dt><dd>110~130㎡</dd></div><div><dt>주차</dt><dd>2대 필수</dd></div><div><dt>입주 희망</dt><dd>2026.12.15까지</dd></div><div><dt>AI 비교 동의</dt><dd>예시 동의 있음</dd></div></dl>
<div class="consent-note"><span aria-hidden="true">i</span><p>상담 동의와 AI 비교 동의는 예시입니다. 홍보 수신은 거부 상태로 두었습니다.</p></div>
<div class="panel-section-heading"><div><span>일정</span><h3>전화 상담 · 예시</h3></div></div>
<article class="appointment-card"><time datetime="2026-08-29T15:30:00+09:00"><strong>29</strong><span>8월 · 토</span></time><div><strong>오후 3:30~4:00</strong><p>맞춤 조건과 권리 확인 순서 · 외부 알림 없음</p><small>한국 표준시 · 데모 일정</small></div><button type="button" data-reschedule>시간 변경 시연</button></article>
<div class="panel-section-heading"><div><span>고객 조건</span><h3>김○○ · 가명 고객</h3></div><span class="status-badge status-demo">연락처 비공개</span></div>
<dl class="data-grid customer-grid"><div><dt>거래 형태</dt><dd>매매</dd></div><div><dt>예산 상한</dt><dd>40억 원 · 예시</dd></div><div><dt>전용 면적</dt><dd>110~130㎡</dd></div><div><dt>주차</dt><dd>2대 필수</dd></div><div><dt>입주 희망</dt><dd>2026.12.15까지</dd></div><div><dt>AI 비교 동의</dt><dd>안내용 동의 있음</dd></div></dl>
<div class="consent-note"><span aria-hidden="true">i</span><p>상담과 AI 비교 동의는 안내를 위한 예시입니다. 홍보 수신은 거부 상태로 표시합니다.</p></div>
<div class="panel-section-heading"><div><span>일정</span><h3>전화 상담 안내</h3></div></div>
<article class="appointment-card"><time datetime="2026-08-29T15:30:00+09:00"><strong>29</strong><span>8월 · 토</span></time><div><strong>오후 3:30~4:00</strong><p>맞춤 조건과 권리 확인 순서 · 알림은 전송되지 않음</p><small>한국 표준시 · 안내 일정</small></div><button type="button" data-reschedule>다른 시간 보기</button></article>
</section>
<section class="tab-panel" role="tabpanel" id="inspector-history" aria-labelledby="tab-history" data-panel="history" hidden>
<div class="panel-section-heading"><div><span>수정 불가 기록</span><h3>변경 이력 8건</h3></div><button type="button" class="text-button" data-simulate-log-failure>실패 상태 보기</button></div>
<div class="panel-section-heading"><div><span>수정 불가 기록</span><h3>변경 이력 8건</h3></div><button type="button" class="text-button" data-simulate-log-failure>기록 확인 안내</button></div>
<ol class="audit-list">
<li><span class="audit-dot is-blocked"></span><div><header><strong>실제 광고 게시 차단</strong><time datetime="2026-08-29T10:42:18+09:00">오늘 10:42</time></header><p>사무소 등록정보와 실제 중개의뢰 관계가 없음</p><small>김유진 · 예시 계정 · 버전 7</small></div></li>
<li><span class="audit-dot"></span><div><header><strong>AI 주장 수정 후 승인</strong><time datetime="2026-08-29T10:31:00+09:00">오늘 10:31</time></header><p>“조용함”을 “현장 확인 필요”로 변경</p><small>김유진 · 예시 계정 · AIRUN-DEMO-0041</small></div></li>
<li><span class="audit-dot"></span><div><header><strong>AI 분석 완료</strong><time datetime="2026-08-29T10:20:00+09:00">오늘 10:20</time></header><p>조건 비교 규칙 demo-0.3 · 입력 버전 고객 3 / 매물 7</p><small>데모 분석기</small></div></li>
<li><span class="audit-dot"></span><div><header><strong>쇼케이스 미리보기 공개</strong><time datetime="2026-08-29T09:54:00+09:00">오늘 09:54</time></header><p>데모 고지와 이미지 출처 6개 항목 통과</p><small>박소연 · 예시 계정</small></div></li>
<li><span class="audit-dot"></span><div><header><strong>모의 서류 추가</strong><time datetime="2026-08-28T17:22:00+09:00">어제 17:22</time></header><p>건축물대장 형식 예시 · 실제 공문서 아님</p><small>김유진 · 예시 계정</small></div></li>
<li><span class="audit-dot is-blocked"></span><div><header><strong>실제 광고 게시 차단</strong><time datetime="2026-08-29T10:42:18+09:00">오늘 10:42</time></header><p>사무소 등록정보와 실제 중개의뢰 관계가 없음</p><small>김유진 · 예시 계정 · 자료 버전 7</small></div></li>
<li><span class="audit-dot"></span><div><header><strong>AI 주장 수정 후 승인</strong><time datetime="2026-08-29T10:31:00+09:00">오늘 10:31</time></header><p>“조용함”을 “현장 확인 필요”로 변경</p><small>김유진 · 예시 계정 · 분석 기록 0041</small></div></li>
<li><span class="audit-dot"></span><div><header><strong>AI 조건 비교 완료</strong><time datetime="2026-08-29T10:20:00+09:00">오늘 10:20</time></header><p>고객 조건과 매물 정보를 안내용으로 대조</p><small>안내용 비교 결과</small></div></li>
<li><span class="audit-dot"></span><div><header><strong>공개 안내 확인</strong><time datetime="2026-08-29T09:54:00+09:00">오늘 09:54</time></header><p>예시 자료 표시와 이미지 출처 6개 항목 확인</p><small>박소연 · 예시 계정</small></div></li>
<li><span class="audit-dot"></span><div><header><strong>참고 자료 추가</strong><time datetime="2026-08-28T17:22:00+09:00">어제 17:22</time></header><p>건축물대장 형식 안내 · 실제 공문서 아님</p><small>김유진 · 예시 계정</small></div></li>
<li><span class="audit-dot"></span><div><header><strong>예시 가격 변경</strong><time datetime="2026-08-28T16:40:00+09:00">어제 16:40</time></header><p>40억 원 → 38억 원 · 가상 의뢰 조건 수정</p><small>김유진 · 예시 계정 · 버전 6 → 7</small></div></li>
<li><span class="audit-dot is-warn"></span><div><header><strong>오래된 자료로 자동 보류</strong><time datetime="2026-08-28T08:00:00+09:00">어제 08:00</time></header><p>DEMO-HJ-103 · 내부 운영 기준일 경과</p><small>데모 일정 작업</small></div></li>
<li><span class="audit-dot is-blocked"></span><div><header><strong>권한 없는 게시 시도 거부</strong><time datetime="2026-08-27T15:16:00+09:00">8월 27일</time></header><p>중개보조 예시 계정은 법정 검수 승인과 게시를 할 수 없음</p><small>권한 검사 · 요청 DEMO-77B</small></div></li>
<li><span class="audit-dot is-warn"></span><div><header><strong>오래된 자료로 자동 보류</strong><time datetime="2026-08-28T08:00:00+09:00">어제 08:00</time></header><p>자료번호 103 · 확인 기준일 경과</p><small>안내 일정 기준</small></div></li>
<li><span class="audit-dot is-blocked"></span><div><header><strong>권한 없는 게시 시도 거부</strong><time datetime="2026-08-27T15:16:00+09:00">8월 27일</time></header><p>중개보조 계정은 법정 검수 승인과 게시를 할 수 없음</p><small>권한 확인</small></div></li>
</ol>
</section>
</div>
<footer class="inspector-actions">
<button type="button" class="button button-ghost" data-focus-missing>누락 항목 보기</button>
<footer class="inspector-actions">
<button type="button" class="button button-ghost" data-focus-missing>확인 항목 보기</button>
<button type="button" class="button button-primary" data-block-publish>실제 광고 게시</button>
</footer>
</aside>
</div>
<section class="role-proof" id="roles" aria-labelledby="roles-title">
<header><div><span>역할에 따라 달라지는 행동</span><h2 id="roles-title">권한 방어선</h2></div><p>버튼을 숨기는 데서 끝내지 않고 게시 시점에 역할, 게이트와 객체 버전을 다시 확인하는 모델입니다.</p></header>
<header><div><span>역할에 따라 달라지는 행동</span><h2 id="roles-title">권한 기준</h2></div><p>게시 전에는 역할, 확인 항목과 최신 자료를 다시 검토합니다.</p></header>
<div class="role-grid"><article><strong>사무소 관리자</strong><p>사무소 연결, 역할, 모든 게시 관리</p><small>감사 로그 수정 불가</small></article><article><strong>공인중개사</strong><p>법정 검수, AI 승인, 게시와 중단</p><small>역할 관리자 변경 불가</small></article><article><strong>중개보조 계정</strong><p>초안, 문의 정리, 일정 등록</p><small>검수 승인과 게시 불가</small></article><article><strong>콘텐츠 편집자</strong><p>설명, 이미지, 미리보기</p><small>가격, 법정 필드와 고객 정보 수정 불가</small></article></div>
</section>
<section class="settings-proof" id="settings" aria-labelledby="settings-title"><h2 id="settings-title">데모 설정 원칙</h2><p>실제 연락, 저장 서버, 광고 채널과 등록정보 연결을 비활성 상태로 유지합니다.</p></section>
<section class="settings-proof" id="settings" aria-labelledby="settings-title"><h2 id="settings-title">예시 자료 안내</h2><p>실제 연락, 저장, 광고 등록과 연결하지 않습니다.</p></section>
</div>
</main>
</div>
<dialog class="admin-dialog" data-block-dialog aria-labelledby="block-dialog-title">
<form method="dialog" class="admin-dialog-shell">
<header><div><span class="dialog-symbol is-blocked" aria-hidden="true">!</span><div><small>PUBLIC_REAL_AD</small><h2 id="block-dialog-title">실제 광고를 게시할 수 없습니다.</h2></div></div><button type="button" data-close-dialog aria-label="게시 차단 안내 닫기">×</button></header>
<header><div><span class="dialog-symbol is-blocked" aria-hidden="true">!</span><div><small>실제 광고 안내</small><h2 id="block-dialog-title">실제 광고를 게시할 수 없습니다.</h2></div></div><button type="button" data-close-dialog aria-label="게시 차단 안내 닫기">×</button></header>
<div class="admin-dialog-content">
<p>이 화면은 가상 사무소와 합성 예시 매물로 만든 쇼케이스입니다. 아래 항목은 더미값으로 채워 통과시키지 않습니다.</p>
<p>이 화면의 사무소와 매물은 예시 자료입니다. 실제 광고에 필요한 항목은 임의의 값으로 채우지 않습니다.</p>
<ol><li><strong>실제 사무소 명칭</strong><span>연결 안 됨</span></li><li><strong>실제 사무소 소재지</strong><span>연결 안 됨</span></li><li><strong>실제 연락처</strong><span>연결 안 됨</span></li><li><strong>실제 등록번호</strong><span>연결 안 됨</span></li><li><strong>개업공인중개사 성명</strong><span>연결 안 됨</span></li><li><strong>실제 중개의뢰 관계</strong><span>없음</span></li><li><strong>실제 거래 가능 상태</strong><span>확인 불가</span></li></ol>
<div class="inline-notice"><span aria-hidden="true">i</span><p>쇼케이스 미리보기 채널은 데모 고지와 생성 이미지 출처를 갖춰 별도로 열 수 있습니다.</p></div>
<div class="inline-notice"><span aria-hidden="true">i</span><p>공개 안내에는 예시 자료 표시와 생성 이미지 출처를 함께 둡니다.</p></div>
</div>
<footer><button type="submit" class="button button-primary" value="close">확인</button></footer>
</form>
@ -280,8 +280,8 @@
<dialog class="admin-dialog preview-dialog" data-preview-dialog aria-labelledby="preview-dialog-title">
<form method="dialog" class="admin-dialog-shell">
<header><div><span class="dialog-symbol is-pass" aria-hidden="true">✓</span><div><small>SHOWCASE</small><h2 id="preview-dialog-title">미리보기 채널은 열 수 있습니다.</h2></div></div><button type="button" data-close-dialog aria-label="쇼케이스 미리보기 안내 닫기">×</button></header>
<div class="admin-dialog-content"><p>상시 데모 고지, 개별 예시 배지, 생성 이미지 출처, 연락 기능 비활성, 실제 주소와 등록번호 없음, AI 데모 표시를 확인했습니다.</p><a class="button button-primary" href="../#listing-detail">공개 상세 보기</a></div>
<header><div><span class="dialog-symbol is-pass" aria-hidden="true">✓</span><div><small>공개 안내</small><h2 id="preview-dialog-title">예시 자료는 공개 화면에서 볼 수 있습니다.</h2></div></div><button type="button" data-close-dialog aria-label="공개 안내 닫기">×</button></header>
<div class="admin-dialog-content"><p>예시 자료 표시, 개별 예시 매물 표기, 생성 이미지 출처, 실제 연락 기능 없음, 실제 주소와 등록번호 없음, AI 결과는 참고용이라는 안내를 확인했습니다.</p><a class="button button-primary" href="../#dossier">공개 상세 보기</a></div>
</form>
</dialog>

View file

@ -0,0 +1,24 @@
# 한남자리 디자인 기록
## 브리프
- 무엇을: 다섯 조건으로 예시 주거 후보를 비교하고 공개 전 확인 책임을 분리해 보여 주는 브리핑과 운영 화면.
- 누구에게: 조건을 정리하는 이용자와 검수 상태를 확인하는 운영 담당자.
- 제약: 가상 매물·AI 참고·생성 이미지·미전송 안내와 기존 선택·검수·이력·대화상자 동작을 보존한다.
## 판단 기준
| 축 | 질문 | 적용 |
| --- | --- | --- |
| Warde · 정보 전달 | 다섯 조건의 라벨·입력값·결과 행동이 한 번에 읽히는가 | 13px 이상 라벨, 16px 입력, 결과 행동을 같은 조건판에 유지 |
| Ruder · 비교 변수 | 후보 조건의 라벨과 값을 섞지 않고 비교하는가 | 조건 행의 라벨·값·보조 메모를 세 단계로 분리 |
| Albers · 상태 맥락 | 가상 자료·AI 참고·중개사 확인의 책임이 구분되는가 | 상태 텍스트와 색조를 함께 사용하고 검수판의 간격·정렬을 통일 |
Warde, Ruder, Albers는 정보 전달·비교 변수·상태 맥락을 검토하는 참고 질문이며 인과나 성능 법칙이 아니다.
## 역할 토큰과 검증
- `--brief-label-size`, `--brief-input-size`, `--brief-panel-radius`, `--brief-rule`, `--admin-body-size`, `--admin-meta-size`가 공개·운영 표면의 역할을 관리한다.
- `verify-final` 18개 묶음은 16개가 즉시 통과했다. 전체 묶음의 invariant·visual 검사는 Gaon·Dure를 대상으로 하며, 나머지 2개는 수정 뒤 release 독립 재실행에서 ALL PASS·ALL MATCH였다.
- root는 28개 모바일, 21개 데스크톱, 7개 하위 경로와 30개 앱 상태를 직접 검토해 수용했다. stress 112건은 hard failure 0이며, 가로 넘침 4건은 targeted fix 뒤 390px 재측정에서 0이다.
- 이 기록은 렌더·계약 범위의 현재 결과다. 모든 폭·키보드 경로·인쇄 등 개별 기능을 완전 재실행했다는 뜻이 아니며, 참여자 과업 효과도 미측정이다.

View file

@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#f3f0e8">
<meta name="description" content="서울 한남 생활권의 집을 예산, 면적, 입주, 주차 조건으로 비교하고 AI 브리핑 뒤 공인중개사가 사실을 다시 확인하는 스마트 중개 디자인 쇼케이스입니다.">
<meta name="description" content="서울 한남 생활권의 집을 예산, 면적, 입주, 주차 조건으로 비교하고 계약 전 공인중개사가 사실을 다시 확인하는 주거 비교 안내입니다.">
<meta property="og:title" content="한남자리 — 내 기준에 맞는 집부터">
<meta property="og:description" content="조건을 정리하고 후보를 비교한 뒤 공인중개사가 확인하는 개인 주거 브리핑 데스크">
<meta property="og:image" content="./og.png">
@ -15,28 +15,27 @@
<title>한남자리 — 내 기준에 맞는 집부터</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%23121311'/%3E%3Ctext x='32' y='39' text-anchor='middle' font-family='Arial' font-size='19' font-weight='700' fill='%23f3f0e8'%3EHJ%3C/text%3E%3C/svg%3E">
<link rel="preload" href="./assets/hannam-evening-1600.webp" as="image" type="image/webp" media="(min-width: 961px)">
<link rel="stylesheet" href="./styles/tokens.css?v=20260829d">
<link rel="stylesheet" href="./styles/landing.css?v=20260829d">
<script src="./scripts/landing.js?v=20260829d" defer></script>
<link rel="stylesheet" href="./styles/tokens.css?v=20260912all">
<link rel="stylesheet" href="./styles/landing.css?v=20260912all">
<script src="./scripts/landing.js?v=20260830e" defer></script>
</head>
<body>
<a class="skip-link" href="#main-content">본문 바로가기</a>
<div class="demo-strip" role="region" aria-label="쇼케이스 고지">디자인 쇼케이스 · 가상 브랜드 · 상담은 접수되지 않습니다</div>
<div class="demo-strip" role="region" aria-label="예시 자료 안내">안내 · 예시 자료 · 입력 내용은 전송되지 않으며 상담은 접수되지 않습니다</div>
<header class="site-header" data-site-header>
<a class="brand-link" href="#top" aria-label="한남자리 처음으로">
<span class="brand-mark" aria-hidden="true">HJ</span>
<span class="brand-lockup"><strong class="brand">한남자리</strong><small>개인 주거 브리핑 데스크</small></span>
</a>
<button class="menu-button" type="button" aria-expanded="false" aria-controls="site-nav" data-menu-button>
<span class="sr-only">메뉴 열기</span><i aria-hidden="true"></i><i aria-hidden="true"></i>
<button class="menu-button" type="button" aria-label="메뉴 열기" aria-expanded="false" aria-controls="site-nav" data-menu-button>
<i aria-hidden="true"></i><i aria-hidden="true"></i>
</button>
<nav class="site-nav" id="site-nav" aria-label="주요 메뉴" data-site-nav>
<a href="#briefing">맞춤 브리핑</a>
<a href="#homes">세 집 비교</a>
<a href="#dossier">선택 서류철</a>
<a href="#handoff">중개사 인계</a>
<a class="nav-admin" href="./admin/">관리 화면</a>
</nav>
</header>
@ -45,7 +44,7 @@
<div class="masthead">
<p class="eyebrow">한남 생활권 맞춤 중개</p>
<h1 id="hero-title">한남에서, 내 기준에 맞는 집부터.</h1>
<p class="hero-copy">예산과 면적, 입주 계획을 먼저 정리합니다. AI가 후보를 비교하고 공인중개사가 다시 확인합니다.</p>
<p class="hero-copy">예산과 면적, 입주 계획을 먼저 정리합니다. 입력한 기준을 후보와 대조하고, 계약 전에는 공인중개사가 다시 확인합니다.</p>
<div class="hero-actions">
<button class="button button-primary" type="button" data-focus-condition>내 조건 분석하기</button>
<a class="text-link" href="#homes">매물 보기 <span aria-hidden="true">→</span></a>
@ -73,8 +72,8 @@
<label class="dock-field"><span>최소 전용면적</span><span class="input-suffix"><input name="minArea" type="number" min="30" max="300" step="1" value="110" inputmode="decimal"><i>㎡</i></span></label>
<label class="dock-field"><span>입주 희망일</span><input name="moveIn" type="date" value="2026-12-20"></label>
<label class="dock-field"><span>필요 주차</span><select name="parking"><option value="0">상관없음</option><option value="1">1대 이상</option><option value="2" selected>2대 이상</option><option value="3">3대 이상</option></select></label>
<div class="dock-actions"><button class="button button-primary" type="submit">분석 결과 보기</button><button class="save-button" type="button" data-save-condition>이 브라우저에 저장</button></div>
<p class="condition-note" id="condition-note">입력은 전송되지 않으며, 저장을 선택하면 이 브라우저에만 보관됩니다.</p>
<div class="dock-actions"><button class="button button-primary" type="submit">분석 결과 보기</button><button class="save-button" type="button" data-save-condition>조건 임시 보관</button></div>
<p class="condition-note" id="condition-note">입력은 전송되지 않으며, 임시 보관을 선택하면 지금 사용 중인 기기에만 남습니다.</p>
</form>
</section>
@ -82,11 +81,11 @@
<header class="section-heading briefing-heading">
<p class="eyebrow">내 조건 브리핑</p>
<h2 id="briefing-title">맞는 이유와<br>조정할 지점을 나눕니다.</h2>
<p>화면에 입력한 값과 후보 정보를 이 브라우저에서 대조합니다.</p>
<p>입력한 기준과 후보 정보를 같은 항목으로 비교합니다.</p>
</header>
<div class="briefing-sheet" data-analysis-result aria-live="polite" aria-atomic="false">
<header><span>예시 AI 브리핑 · 모의 조건 비교 · 공인중개사 확인 전</span><strong data-analysis-home>아직 분석하지 않았습니다</strong></header>
<header><span>안내용 조건 비교 · 공인중개사 확인 전</span><strong data-analysis-home>아직 분석하지 않았습니다</strong></header>
<div class="briefing-empty" data-analysis-empty>
<span aria-hidden="true">01</span>
<div><h3>내 조건을 먼저 알려 주세요.</h3><p>분석을 실행하면 세 후보 가운데 조건과 가까운 집을 고르고, 이유를 세 묶음으로 정리합니다.</p></div>
@ -195,19 +194,19 @@
<li><span>02</span><div><strong>후보 비교</strong><p>서로 다른 집의 차이를 같은 기준으로 대조합니다.</p></div></li>
<li><span>03</span><div><strong>권리·현장 확인</strong><p>서류와 현장에서 확인한 뒤 최종 판단합니다.</p></div></li>
</ol>
<button class="button button-light handoff-cta" type="button" data-open-consult>상담 흐름 미리보기</button>
<button class="button button-light handoff-cta" type="button" data-open-consult>상담 전 확인할 항목 보기</button>
</section>
</main>
<footer class="site-footer">
<div><strong>한남자리</strong><span>개인 주거 브리핑 데스크</span></div>
<p>한남자리는 가상 브랜드이며 등록 공인중개사무소나 거래 서비스를 제공하지 않습니다.</p>
<nav aria-label="보조 메뉴"><a href="#top">처음으로</a><a href="./admin/">관리 화면</a></nav>
<p>한남자리는 예시 자료로 구성한 서비스 화면입니다. 등록 공인중개사무소나 거래 서비스를 제공하지 않습니다.</p>
<nav aria-label="보조 메뉴"><a href="#top">처음으로</a></nav>
</footer>
<dialog class="consult-dialog" data-consult-dialog aria-labelledby="consult-title">
<form method="dialog" class="dialog-shell" data-consult-form>
<header><div><span>상담 순서 살펴보기</span><h2 id="consult-title">중개사에게 무엇을 넘길까요?</h2></div><button class="dialog-close" type="button" aria-label="상담 흐름 닫기" data-close-consult>×</button></header>
<header><div><span>상담 전 확인 항목</span><h2 id="consult-title">중개사와 무엇을 먼저 확인할까요?</h2></div><button class="dialog-close" type="button" aria-label="상담 안내 닫기" data-close-consult>×</button></header>
<p class="dialog-privacy">연락처를 받거나 입력 내용을 저장하지 않습니다.</p>
<div class="dialog-content" data-consult-content>
<p><strong data-consult-home>정원형 연립주택</strong>을 기준으로 준비할 항목을 골라 보세요.</p>
@ -220,6 +219,6 @@
</dialog>
<div class="live-region" role="status" aria-live="polite" data-live-region hidden></div>
<noscript><div class="no-script"><strong>자바스크립트 없이 보는 중입니다.</strong><p>집 정보는 읽을 수 있지만 조건 비교, 후보 변경과 상담 미리보기는 작동하지 않습니다.</p></div></noscript>
<noscript><div class="no-script"><strong>자바스크립트 없이 보는 중입니다.</strong><p>집 정보는 읽을 수 있지만 조건 비교, 후보 변경과 상담 준비 순서는 이용할 수 없습니다.</p></div></noscript>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 KiB

After

Width:  |  Height:  |  Size: 292 KiB

Before After
Before After

View file

@ -22,7 +22,7 @@
},
"DEMO-HJ-102": {
title: "한강 가까운 복층 주거",
status: "쇼케이스 공개",
status: "예시 공개",
statusClass: "status-pass",
trade: "전세",
type: "공동주택",
@ -32,7 +32,7 @@
moveIn: "2026.11.30 이후 협의",
image: "../assets/hannam-evening-640.webp",
imageAlt: "서울 언덕길의 석재 주거 건물 연출 이미지",
blockers: "데모 통과",
blockers: "안내 확인",
ai: "수정 후 승인"
},
"DEMO-HJ-103": {
@ -57,7 +57,7 @@
trade: "매매",
type: "단독주택",
price: "상담 종료",
area: "예시 공개 종료",
area: "목록에서 제외",
rooms: "공개 목록 제외",
moveIn: "해당 없음",
image: "../assets/hannam-evening-640.webp",
@ -162,7 +162,7 @@
if (!inspector) return;
inspector.classList.add("is-open");
workbench?.classList.remove("is-inspector-closed");
$("[data-inspector-id]", inspector).textContent = `${id} · 예시 매물`;
$("[data-inspector-id]", inspector).textContent = `자료번호 ${id.replace("DEMO-HJ-", "")} · 예시 매물`;
$("[data-inspector-title]", inspector).textContent = detail.title;
const status = $("[data-inspector-status]", inspector);
status.textContent = detail.status;
@ -180,7 +180,7 @@
if (basicGrid) {
const pairs = [
["거래 형태", detail.trade],
["예시 가격", detail.price],
["표시 가격", detail.price],
["유형", detail.type],
["전용 면적", detail.area],
["방과 욕실", detail.rooms],
@ -199,7 +199,7 @@
updateInspectorBackdrop();
if (focus) $("[data-close-inspector]", inspector)?.focus();
announce(`${detail.title} 조사관을 열었습니다. ${detail.status} 상태입니다.`);
announce(`${detail.title} 상세 정보를 열었습니다. ${detail.status} 상태입니다.`);
};
const closeInspector = ({ restoreFocus = false } = {}) => {
@ -216,7 +216,7 @@
if (restoreFocus && previousId) {
$(`[data-queue-row][data-id="${previousId}"] [data-select-row]`)?.focus();
}
announce("조사관과 선택 행을 함께 닫았습니다.");
announce("선택한 매물 정보를 닫았습니다.");
};
const activateTab = (name, focus = false) => {
@ -258,7 +258,7 @@
const updateResultCount = () => {
const visible = getRows().filter((row) => !row.classList.contains("is-hidden"));
const result = $("[data-result-count]");
if (result) result.textContent = `${visible.length}개 예시`;
if (result) result.textContent = `${visible.length}건`;
const empty = $("[data-queue-empty]");
if (state.mode === "normal") empty.hidden = visible.length !== 0;
return visible.length;
@ -276,7 +276,7 @@
const emptyTitle = $("[data-empty-title]");
const emptyCopy = $("[data-empty-copy]");
if (count === 0 && state.mode === "normal") {
emptyTitle.textContent = "현재 조건에 맞는 예시가 없습니다.";
emptyTitle.textContent = "현재 조건에 맞는 항목이 없습니다.";
emptyCopy.textContent = "검색어와 필터를 초기화하면 전체 대기열을 다시 볼 수 있습니다.";
}
};
@ -320,9 +320,9 @@
if (mode === "empty") {
const empty = $("[data-queue-empty]");
empty.hidden = false;
$("[data-empty-title]").textContent = "아직 접수된 예시 매물이 없습니다.";
$("[data-empty-copy]").textContent = "새 예시 초안을 만들거나 다른 상태를 시연할 수 있습니다.";
announce("빈 대기열 상태를 시연합니다.");
$("[data-empty-title]").textContent = "아직 표시할 항목이 없습니다.";
$("[data-empty-copy]").textContent = "검색어와 필터를 초기화하면 전체 목록을 다시 볼 수 있습니다.";
announce("현재 표시할 항목이 없습니다.");
return;
}
@ -333,8 +333,8 @@
}
if (mode === "save-error") {
showBanner("저장 경로를 확인할 수 없습니다.", "입력은 화면에 남지만 저장 성공으로 표시하지 않습니다. 수정 시연을 누르면 실패 처리를 확인할 수 있습니다.", "error");
announce("저장 실패 상태를 시연합니다.");
showBanner("지금은 변경 내용을 보관할 수 없습니다.", "입력은 화면에 남지만 완료로 표시하지 않습니다.", "error");
announce("저장할 수 없는 상태입니다.");
return;
}
@ -347,25 +347,25 @@
status.textContent = "자료 오래됨";
status.className = "status-badge status-warn";
}
announce("자료 오래됨 상태를 시연합니다.");
announce("자료가 오래되어 다시 확인이 필요합니다.");
return;
}
if (mode === "conflict") {
showBanner("다른 예시 담당자가 먼저 수정했습니다.", "현재 버전 7과 서버 예시 버전 8의 차이를 확인한 뒤 다시 저장해야 합니다.", "error");
announce("버전 충돌 상태를 시연합니다.");
showBanner("다른 담당자가 먼저 수정했습니다.", "현재 내용과 최신 자료의 차이를 확인한 뒤 다시 저장해야 합니다.", "error");
announce("최신 자료와 차이가 있어 다시 확인이 필요합니다.");
return;
}
if (mode === "permission") {
document.body.classList.add("is-permission-limited");
$$('[data-review-ai], [data-simulate-save], [data-new-draft]').forEach((button) => button.disabled = true);
showBanner("중개보조 예시 권한으로 보는 중입니다.", "초안과 일정은 정리할 수 있지만 법정 검수 승인, AI 최종 승인과 게시를 할 수 없습니다.", "info");
announce("권한 제한 상태를 시연합니다.");
showBanner("중개보조 권한으로 보는 중입니다.", "초안과 일정은 정리할 수 있지만 법정 검수 승인, AI 최종 승인과 게시를 할 수 없습니다.", "info");
announce("현재 권한으로는 승인과 게시를 할 수 없습니다.");
return;
}
announce("정상 데모 상태로 돌아왔습니다.");
announce("기본 목록을 다시 보여 줍니다.");
};
const openDialog = (dialog) => {
@ -380,11 +380,11 @@
const simulateSave = () => {
if (state.mode === "permission") {
announce("현재 예시 권한으로는 매물 정보를 수정할 수 없습니다.");
announce("현재 권한으로는 매물 정보를 수정할 수 없습니다.");
return;
}
if (state.mode === "conflict") {
showBanner("버전 충돌로 저장하지 않았습니다.", "서버 예시 버전 8을 다시 불러오거나 현재 수정을 별도 초안으로 남겨야 합니다.", "error");
showBanner("최신 자료와 차이가 있어 저장하지 않았습니다.", "최신 내용을 다시 확인하거나 현재 수정을 별도 초안으로 남겨야 합니다.", "error");
announce("버전 충돌로 저장하지 않았습니다.");
return;
}
@ -393,12 +393,12 @@
announce("저장에 실패했습니다. 성공으로 처리하지 않았습니다.");
return;
}
announce("수정 시연을 마쳤습니다. 서버가 없는 데모라 실제 저장하지 않았습니다.");
announce("변경 내용을 확인했습니다. 실제로 저장되거나 전송되지는 않습니다.");
};
const createDraft = () => {
if (state.mode === "permission") {
announce("현재 예시 권한으로는 새 초안을 만들 수 없습니다.");
announce("현재 권한으로는 새 초안을 만들 수 없습니다.");
return;
}
if (state.draftCreated) {
@ -407,7 +407,7 @@
}
state.draftCreated = true;
listingDetails["DEMO-HJ-NEW"] = {
title: "제목 없는 새 예시 초안",
title: "제목 없는 새 항목",
status: "초안",
statusClass: "status-demo",
trade: "미선택",
@ -427,15 +427,15 @@
row.dataset.id = "DEMO-HJ-NEW";
row.dataset.status = "draft";
row.dataset.updated = new Date().toISOString();
row.dataset.title = "제목 없는 새 예시 초안";
row.dataset.title = "제목 없는 새 항목";
row.setAttribute("aria-selected", "false");
row.innerHTML = `
<td class="row-cell-select"><button type="button" class="row-select-overlay" data-select-row aria-label="제목 없는 새 예시 초안 조사관 열기"></button><span class="row-title" aria-hidden="true"><span class="row-thumb row-thumb-draft"><i></i></span><span><strong>제목 없는 새 예시 초안</strong><small>DEMO-HJ-NEW · 예시 매물</small></span></span></td>
<td><span>미선택</span><small>유형 미선택</small></td><td><strong>입력 전</strong><small>면적 입력 전</small></td><td><span>방금</span><small>데모 초안</small></td><td><span class="cell-status is-blocked"><i aria-hidden="true"></i>입력 대기</span></td><td><span class="cell-status is-muted"><i aria-hidden="true"></i>실행 전</span></td><td><span class="assignee"><i>김</i>김유진</span></td>`;
<td class="row-cell-select"><button type="button" class="row-select-overlay" data-select-row aria-label="제목 없는 새 항목 상세 정보 열기"></button><span class="row-title" aria-hidden="true"><span class="row-thumb row-thumb-draft"><i></i></span><span><strong>제목 없는 새 항목</strong><small>새 예시 매물</small></span></span></td>
<td><span>미선택</span><small>유형 미선택</small></td><td><strong>입력 전</strong><small>면적 입력 전</small></td><td><span>방금</span><small>새 항목</small></td><td><span class="cell-status is-blocked"><i aria-hidden="true"></i>입력 대기</span></td><td><span class="cell-status is-muted"><i aria-hidden="true"></i>분석 전</span></td><td><span class="assignee"><i>김</i>김유진</span></td>`;
queueBody.prepend(row);
applyFilters();
openInspector("DEMO-HJ-NEW", true);
announce("새 예시 초안을 만들었습니다. 외부 서버에는 저장하지 않았습니다.");
announce("새 항목을 만들었습니다. 실제로 저장되거나 전송되지는 않습니다.");
};
const handleRowSelection = (button) => {
@ -523,7 +523,7 @@
$("[data-review-ai]")?.addEventListener("click", (event) => {
if (state.mode === "permission") {
announce("현재 예시 권한으로는 AI 결과를 승인할 수 없습니다.");
announce("현재 권한으로는 AI 결과를 승인할 수 없습니다.");
return;
}
event.currentTarget.textContent = "수정 후 승인됨";
@ -533,20 +533,20 @@
badge.textContent = "수정 후 승인";
badge.className = "status-badge status-pass";
}
announce("AI 참고 분석을 수정 후 승인 상태로 바꿨습니다. 데모 외부에는 게시하지 않았습니다.");
announce("AI 참고 결과를 수정 후 확인 상태로 바꿨습니다. 실제 광고에는 사용되지 않습니다.");
});
$$('[data-claim-action]').forEach((button) => button.addEventListener("click", () => {
button.textContent = button.dataset.claimAction === "edit" ? "수정 반영" : "확인됨";
button.disabled = true;
announce("주장 검토 상태를 데모 안에서만 바꿨습니다.");
announce("표시 문구의 검토 상태를 이 화면에서만 바꿨습니다.");
}));
$("[data-reschedule]")?.addEventListener("click", (event) => {
const card = event.currentTarget.closest(".appointment-card");
const time = $("div > strong", card);
time.textContent = time.textContent.includes("3:30") ? "오후 5:00~5:30" : "오후 3:30~4:00";
announce("예시 상담 시간을 바꿨습니다. 외부 알림은 보내지 않았습니다.");
announce("안내용 상담 시간을 바꿨습니다. 알림은 전송되지 않았습니다.");
});
$("[data-simulate-log-failure]")?.addEventListener("click", () => {

View file

@ -386,7 +386,7 @@
state.selectedId = best.id;
renderSelection();
$("#briefing")?.scrollIntoView({ behavior: window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth" });
announce(`${best.name}을 가장 가까운 후보로 골라 세 묶음의 브리핑을 만들었습니다.`);
announce(`${best.name}을 가장 가까운 후보로 골랐습니다. 맞는 점, 조정할 점, 확인할 점을 정리했습니다.`);
};
const markConditionsChanged = () => {
@ -400,9 +400,9 @@
if (!form.reportValidity()) return;
try {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(readConditions()));
announce("현재 조건을 이 브라우저에 저장했습니다.");
announce("현재 조건을 지금 사용 중인 기기에 임시 보관했습니다.");
} catch {
announce("브라우저 저장소를 사용할 수 없어 조건을 저장하지 않았습니다.");
announce("지금 사용 중인 기기에 조건을 보관할 수 없습니다.");
}
};

View file

@ -1,5 +1,5 @@
.admin-body {
min-width: 20rem;
min-width: 0;
color: #1c1c1a;
background: #f3f4f3;
}
@ -237,7 +237,7 @@
display: inline-flex;
align-items: center;
gap: 0.55rem;
padding: 0.42rem 0.6rem;
padding: 0.42rem 0.7rem;
}
.topbar-actions .icon-button {
@ -390,10 +390,10 @@
gap: 0.5rem;
align-items: center;
margin-bottom: 0.75rem;
padding: 0.7rem;
padding: 0.875rem;
background: #fff;
border: 1px solid #dedfdd;
border-radius: 0.5rem;
border: 1px solid var(--brief-rule);
border-radius: var(--brief-panel-radius);
}
.search-field {
@ -412,6 +412,7 @@
width: 100%;
min-height: 2.75rem;
min-width: 0;
padding-inline: 0.5rem;
color: #222320;
background: transparent;
border: 0;
@ -419,6 +420,11 @@
font-size: 0.72rem;
}
.search-field:focus-within {
border-color: var(--focus);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--focus) 32%, transparent);
}
.queue-toolbar select {
min-height: 2.75rem;
padding: 0.42rem 1.8rem 0.42rem 0.65rem;
@ -459,7 +465,8 @@
.settings-proof {
background: #fff;
border: 1px solid #dedfdd;
border-radius: 0.55rem;
border-radius: var(--brief-panel-radius);
box-shadow: none;
}
.queue-panel {
@ -522,7 +529,11 @@
}
.table-scroll {
min-width: 0;
max-width: 100%;
overflow: auto;
overscroll-behavior-inline: contain;
scrollbar-gutter: stable both-edges;
scrollbar-width: thin;
}
@ -535,9 +546,9 @@
.queue-table th,
.queue-table td {
padding: 0.7rem 0.6rem;
padding: 0.8rem 0.875rem;
border-bottom: 1px solid #ecedea;
font-size: 0.66rem;
font-size: var(--admin-body-size);
text-align: left;
vertical-align: middle;
}
@ -548,7 +559,7 @@
top: 0;
color: #777973;
background: #fafbfa;
font-size: 0.58rem;
font-size: var(--admin-meta-size);
font-weight: 730;
}
@ -605,7 +616,7 @@
.queue-table td > strong + small {
margin-top: 0.13rem;
color: #62665f;
font-size: 0.57rem;
font-size: var(--admin-meta-size);
}
.row-title {
@ -822,6 +833,13 @@
to { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
.queue-loading > span {
animation: none;
opacity: 0.72;
}
}
.queue-empty > span {
display: grid;
width: 3rem;
@ -1087,7 +1105,7 @@
.text-button {
min-width: 2.75rem;
min-height: 2.75rem;
padding: 0.4rem;
padding: 0.5rem;
color: #6f5420;
background: transparent;
border: 0;
@ -1510,23 +1528,26 @@
.claim-review article {
margin-bottom: 0.5rem;
padding: 0.65rem;
padding: 0.75rem;
background: #fafbfa;
border: 1px solid #e1e2df;
border: 1px solid var(--brief-rule);
border-left-width: 3px;
border-radius: 0.35rem;
border-radius: var(--brief-panel-radius);
}
.claim-review article.is-fact {
border-left-color: #3d7c5d;
background: var(--review-fact-bg);
}
.claim-review article.is-unknown {
border-left-color: #b1832d;
background: var(--review-unknown-bg);
}
.claim-review article.is-edited {
border-left-color: #9d3f34;
background: var(--review-edited-bg);
}
.claim-review header {
@ -1791,9 +1812,15 @@
}
.admin-dialog {
position: fixed;
inset: 0;
box-sizing: border-box;
width: min(35rem, calc(100% - 2rem));
max-height: calc(100dvh - 2rem);
margin: auto;
padding: 0;
overflow: auto;
overscroll-behavior: contain;
color: #20211f;
background: #fff;
border: 0;
@ -1801,6 +1828,10 @@
box-shadow: 0 1.5rem 5rem rgba(20, 20, 19, 0.28);
}
html:has(.admin-dialog[open]) {
overflow: hidden;
}
.admin-dialog::backdrop {
background: rgba(20, 20, 19, 0.62);
backdrop-filter: blur(2px);
@ -1923,6 +1954,13 @@
font-size: 0.65rem;
}
@media (min-width: 120rem) {
.admin-content {
max-width: 122rem;
padding: 2rem;
}
}
@media (max-width: 92rem) {
.admin-workbench {
grid-template-columns: minmax(35rem, 1fr) 23rem;
@ -1977,6 +2015,33 @@
}
}
/* 짧은 가로 화면은 조사관을 덮는 시트로 다뤄야 한다.
미디어·부제까지 고정하면 scroll 영역이 0px로 수축해 핵심 검토 내용이 전부 사라진다. */
@media (max-width: 76rem) and (max-height: 40rem) {
.inspector {
top: 0;
max-height: 100dvh;
}
.inspector-header {
padding: 0.65rem 0.8rem;
}
.inspector-header small,
.inspector-media {
display: none;
}
.inspector-tabs button {
min-height: 2.25rem;
padding-block: 0.45rem;
}
.inspector-actions {
padding: 0.5rem;
}
}
@media (max-width: 64rem) {
.admin-shell {
grid-template-columns: 13.5rem minmax(0, 1fr);
@ -2034,11 +2099,29 @@
}
.admin-topbar {
min-width: 0;
gap: 0.5rem;
padding: 0 0.8rem;
}
.breadcrumb {
margin-right: auto;
min-width: 0;
overflow-wrap: anywhere;
}
.topbar-actions {
min-width: 0;
flex: 0 0 auto;
}
.account-button {
display: grid;
width: 2.75rem;
min-width: 0;
flex: 0 0 2.75rem;
place-items: center;
padding: 0;
}
.icon-button kbd,
@ -2087,7 +2170,7 @@
@media (max-width: 34rem) {
.demo-strip {
min-height: 2.3rem;
padding-inline: 0.5rem;
padding-inline: 0.75rem;
}
.admin-sidebar,
@ -2141,6 +2224,29 @@
}
}
/* 이 규칙은 좁은 폭 보정 뒤에 둬야 한다. 그렇지 않으면 34rem 보정의 top 값이
짧은 화면 시트보다 뒤에 적용되어 스크롤 영역이 다시 0px가 된다. */
@media (max-width: 76rem) and (max-height: 40rem) {
.inspector {
top: 0;
max-height: 100dvh;
}
.channel-gates {
grid-template-columns: repeat(2, minmax(0, 1fr));
padding: 0.35rem;
gap: 0.35rem;
}
.channel-gate {
padding: 0.35rem 0.4rem;
}
.channel-gate small {
display: none;
}
}
/* 작은 운영 문구도 본문 대비 기준을 넘도록 중립색을 한 단계 진하게 쓴다. */
.admin-main small,
.admin-main dt,

View file

@ -100,12 +100,6 @@ body {
color: var(--slate-deep);
}
.site-nav .nav-admin {
padding: 0 0.95rem;
color: var(--sheet);
background: var(--charcoal);
}
.menu-button {
display: none;
width: 2.75rem;
@ -165,6 +159,7 @@ body {
.brief-hero {
display: grid;
min-width: 0;
width: min(100%, 96rem);
margin: 0 auto;
padding: 3rem 2rem 0;
@ -292,12 +287,14 @@ body {
grid-area: dock;
display: grid;
grid-template-columns: minmax(10rem, 1.15fr) repeat(5, minmax(7.5rem, 0.8fr)) minmax(10rem, 1fr);
gap: 0.8rem 1rem;
min-width: 0;
gap: 1rem;
align-items: end;
margin: -5.5rem 2.5rem 0;
padding: 1.35rem 1.5rem 1rem;
background: var(--sheet);
border-top: 3px solid var(--charcoal);
border-bottom: 1px solid var(--brief-rule);
box-shadow: 0 6px 0 var(--paper-shadow), 0 1.4rem 3rem rgba(44, 45, 40, 0.1);
}
@ -312,34 +309,35 @@ body {
.dock-heading span {
color: var(--slate-deep);
font-size: 0.62rem;
font-size: var(--brief-label-size);
font-weight: 760;
}
.dock-heading strong {
margin-top: 0.2rem;
font-size: 0.8rem;
font-size: 0.9375rem;
}
.dock-field > span:first-child {
display: block;
margin-bottom: 0.3rem;
color: #646963;
font-size: 0.61rem;
font-size: var(--brief-label-size);
font-weight: 710;
}
.dock-field input,
.dock-field select {
width: 100%;
min-height: 2.75rem;
padding: 0.55rem 0.4rem;
min-height: 3rem;
padding: 0.55rem 0.5rem;
color: var(--charcoal);
background: transparent;
border: 0;
border-bottom: 1px solid #90948e;
border-radius: 0;
font-size: 0.78rem;
font-size: var(--brief-input-size);
font-variant-numeric: tabular-nums;
font-weight: 710;
}
@ -373,7 +371,7 @@ body {
.save-button {
min-height: 2.75rem;
padding: 0.35rem;
padding: 0.5rem;
color: #555b56;
background: transparent;
border: 0;
@ -409,6 +407,17 @@ body {
gap: clamp(3rem, 7vw, 7rem);
}
/* 1920px 이상에서는 비교·서류 표면만 조금 넓혀 여백이 본문을 압도하지 않게 한다. */
@media (min-width: 120rem) {
.brief-hero {
width: min(100%, 104rem);
}
.briefing-section {
width: min(100% - 4rem, 100rem);
}
}
.briefing-heading {
align-self: center;
}
@ -537,25 +546,34 @@ body {
.compare-heading {
display: grid;
min-width: 0;
align-items: end;
grid-template-columns: minmax(28rem, 1fr) minmax(17rem, 0.5fr) auto;
grid-template-columns: minmax(0, 1fr) minmax(0, 0.5fr) minmax(0, auto);
gap: 3rem;
margin-bottom: 3.5rem;
}
.compare-heading > p {
min-width: 0;
margin: 0 0 0.45rem;
color: #5a605b;
font-size: 0.8rem;
}
.compare-heading h2 {
overflow-wrap: anywhere;
word-break: normal;
}
.unit-switches {
display: flex;
min-width: 0;
gap: 0.6rem;
}
.unit-switches > div {
display: flex;
min-width: 0;
border: 1px solid var(--hairline);
}
@ -1035,8 +1053,9 @@ body {
.handoff-section {
display: grid;
min-width: 0;
align-items: center;
grid-template-columns: minmax(24rem, 0.8fr) minmax(28rem, 1fr) auto;
grid-template-columns: minmax(0, 0.8fr) minmax(0, 1fr) minmax(0, auto);
gap: clamp(3rem, 7vw, 7rem);
padding: clamp(6rem, 9vw, 9rem) max(2rem, calc((100% - 92rem) / 2));
color: var(--sheet);
@ -1047,6 +1066,18 @@ body {
color: #bec7cc;
}
.handoff-section > header,
.handoff-steps,
.handoff-cta {
min-width: 0;
}
.handoff-section h2,
.handoff-steps p,
.handoff-cta {
overflow-wrap: anywhere;
}
.handoff-steps {
margin: 0;
padding: 0;
@ -1122,9 +1153,15 @@ body {
}
.consult-dialog {
position: fixed;
inset: 0;
box-sizing: border-box;
width: min(36rem, calc(100% - 2rem));
max-height: calc(100svh - 2rem);
margin: auto;
padding: 0;
overflow: auto;
overscroll-behavior: contain;
color: var(--charcoal);
background: var(--sheet);
border: 1px solid var(--charcoal);
@ -1132,6 +1169,10 @@ body {
box-shadow: 0 2rem 5rem rgba(0, 0, 0, 0.28);
}
html:has(.consult-dialog[open]) {
overflow: hidden;
}
.consult-dialog::backdrop {
background: rgba(18, 19, 17, 0.72);
}
@ -1298,12 +1339,16 @@ body {
z-index: 90;
top: 6.5rem;
right: 0;
bottom: 0;
left: 0;
display: none;
align-items: stretch;
flex-direction: column;
gap: 0;
max-height: calc(100dvh - 6.5rem);
padding: 0.75rem 1.25rem 1.25rem;
overflow-y: auto;
overscroll-behavior: contain;
background: var(--sheet);
border-bottom: 1px solid var(--hairline);
}
@ -1312,8 +1357,7 @@ body {
display: flex;
}
.site-nav a,
.site-nav .nav-admin {
.site-nav a {
min-height: 3rem;
padding: 0 0.5rem;
color: var(--charcoal);
@ -1324,15 +1368,15 @@ body {
.brief-hero {
padding: 0;
grid-template-areas: "masthead" "scene" "dock";
grid-template-columns: 1fr;
grid-template-columns: minmax(0, 1fr);
}
.masthead {
padding: 5rem max(1.25rem, calc((100% - 44rem) / 2)) 4rem;
padding: 5rem max(1.25rem, calc((100% - 44rem) / 2)) var(--space-10);
}
.hero-scene {
height: min(70vw, 40rem);
height: min(70vw, 38rem);
min-height: 32rem;
}
@ -1371,7 +1415,7 @@ body {
}
.compare-heading {
grid-template-columns: 1fr auto;
grid-template-columns: minmax(0, 1fr) minmax(0, auto);
}
.compare-heading > p {
@ -1439,7 +1483,7 @@ body {
}
.handoff-section {
grid-template-columns: 1fr;
grid-template-columns: minmax(0, 1fr);
}
.handoff-cta {
@ -1456,6 +1500,7 @@ body {
}
@media (max-width: 40rem) {
.handoff-cta { width: 100%; min-width: 0; }
.demo-strip {
font-size: 0.62rem;
}
@ -1576,7 +1621,7 @@ body {
}
.compare-heading {
grid-template-columns: 1fr;
grid-template-columns: minmax(0, 1fr);
gap: 1.5rem;
margin-bottom: 2.5rem;
}

View file

@ -51,6 +51,15 @@
--radius-md: 0.75rem;
--radius-lg: 1.25rem;
--transition: 180ms cubic-bezier(0.22, 1, 0.36, 1);
--brief-label-size: 0.8125rem;
--brief-input-size: 1rem;
--brief-panel-radius: 0.5rem;
--brief-rule: var(--line-light);
--admin-body-size: 0.8125rem;
--admin-meta-size: 0.75rem;
--review-fact-bg: #f2f8f4;
--review-unknown-bg: #fff8e8;
--review-edited-bg: #fdf0ed;
}
*,

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,13 @@
# MaruBuri source record
Downloaded 2026-09-12 from the official NAVER Hangeul static host. The files are retained unchanged: no conversion or subsetting was performed.
| File | Official URL | HTTP content type | Size | SHA-256 |
| --- | --- | --- | --- | --- |
| `MaruBuri-Regular.woff2` | https://hangeul.pstatic.net/hangeul_static/webfont/MaruBuri/MaruBuri-Regular.woff2 | `font/woff2` | 433776 bytes | `4CF1341CF2F23FB3E263712DFDE1D8F25EEDCC328B696A2E5A2C8ADD55E5C17B` |
| `MaruBuri-SemiBold.woff2` | https://hangeul.pstatic.net/hangeul_static/webfont/MaruBuri/MaruBuri-SemiBold.woff2 | `font/woff2` | 466108 bytes | `7AEF4862FA3219B0A7DEB6746E27116A38C4069F4424EB51EDF103B29795F5C2` |
- Official stylesheet checked: https://hangeul.pstatic.net/hangeul_static/css/maru-buri.css
- Official NAVER license page saved verbatim as `LICENSE.html`: https://help.naver.com/service/11029/contents/18088?lang=ko&osType=PC
- `LICENSE.html` SHA-256: `8538F7921744D5E040F603BC301465EFBD038F1054D0754B7508DE3286452EC3`
- The saved official page includes the NAVER copyright notice and the SIL Open Font License 1.1 text for MaruBuri.

View file

@ -0,0 +1,17 @@
# 향연
연기 세로 사진과 주변 여백을 유지한다. 히어로 사진의 이중 액자를 제거해 사진면과 기록면을 직접 맞대고, 숙성 장부와 아카이브 목록은 라벨·값이 같은 기준선에서 읽히게 조정한다. 본문은 17px 이상과 42rem 이하의 폭을 쓴다.
| 판단 축 | 적용 | 한계 |
| --- | --- | --- |
| 구도 | Hofmann의 면과 여백 관계를 참고해 사진 주위의 불필요한 액자 여백을 줄였다. | 이 참고가 모든 전시형 페이지의 최적 해법은 아니다. |
| 타입 | Ruder를 참고해 장부의 라벨·값과 본문의 역할을 분리했다. | 읽기 속도나 이해도 향상을 주장하지 않는다. |
| 색 | Albers를 참고해 따뜻한 바탕과 적갈색 동작색의 역할 경계를 유지했다. | 대비 수치는 브라우저 검증으로 재확인한다. |
토큰은 `--surface`, `--ink`, `--accent`, `--step-0`, `--space-*`를 사용한다. 로컬 MaruBuri 400/600은 `styles/fonts.css`, Pretendard는 기존 CDN을 유지한다. 가상 기록·예시 가격·기존 앵커는 바꾸지 않았다.
## 2026-09-12 현재 검증 기록
- `verify-final` 18개 묶음은 16개가 즉시 통과했다. 전체 묶음의 invariant·visual 검사는 Gaon·Dure를 대상으로 하며, 나머지 2개는 수정 뒤 release 독립 재실행에서 ALL PASS·ALL MATCH였다.
- root는 28개 모바일, 21개 데스크톱, 7개 하위 경로와 30개 앱 상태를 직접 검토해 수용했다. stress 112건은 hard failure 0이며, 가로 넘침 4건은 targeted fix 뒤 390px 재측정에서 0이다.
- 이 기록은 렌더·계약 범위의 현재 결과다. 모든 폭·키보드 경로·인쇄 등 개별 기능을 완전 재실행했다는 뜻이 아니며, 참여자 과업 효과도 미측정이다.

View file

@ -16,13 +16,13 @@
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/webfontworld/naver/MaruBuri.css"
href="./styles/fonts.css?v=20260912all"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css"
/>
<link rel="stylesheet" href="./styles/main.css?v=2" />
<link rel="stylesheet" href="./styles/main.css?v=20260912all" />
<link
rel="icon"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23F6F2E9'/%3E%3Cpath d='M16 6c0 3 0 5 0 7m0 4c0 3 2 3 2 5m-2 2c0 2-2 3-2 5' fill='none' stroke='%237C3B24' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E"
@ -45,24 +45,20 @@
</header>
<main id="main">
<!--
히어로 — 이 페이지의 리스크: 상품 사진 대신 연기 한 줄기.
여백이 주인공이고 사진은 그 여백을 가르는 흔적 하나다.
-->
<section class="hero" aria-labelledby="hero-h">
<div class="wrap hero-grid">
<div class="hero-text">
<p class="hero-kicker reveal">자연 침향 · 백단 · 서울 삼청동</p>
<p class="hero-kicker reveal">가상 향 아카이브 · 기록 형식 예시</p>
<h1 id="hero-h" class="reveal" style="--delay: 80ms">
연기는 흩어지고<br />향기만 머뭅니다
</h1>
<p class="lead reveal" style="--delay: 160ms">
향연은 향을 판매하기 전에 기록부터 남깁니다. 어느 해에 들인 나무인지,
수지가 몇이었는지, 연소에 몇 분이 걸렸는지. 향은 사라지는 것이니
남는 것은 문서뿐입니다.
향연은 향의 산지·숙성·연소 정보를 어떻게 기록할 수 있는지 보여 주는
가상 아카이브입니다. 아래 상품·가격·방문 정보는 판매나 예약을 위한
정보가 아니라 기록 형식의 예시입니다.
</p>
<div class="hero-actions reveal" style="--delay: 220ms">
<a class="btn" href="#collection">아카이브 보기</a>
<a class="btn" href="#collection">기록 구성 보기</a>
</div>
</div>
<figure class="hero-photo reveal" style="--delay: 120ms">
@ -74,15 +70,11 @@
loading="eager"
fetchpriority="high"
/>
<figcaption>침향 한 줄기 · 삼청동 아카이브룸</figcaption>
<figcaption>침향 연출 이미지 · 가상 아카이브</figcaption>
</figure>
</div>
</section>
<!--
향목 — 나무의 단면. 침향은 나무가 아니라 나무의 병(병해수지)이라는 것을
이 섹션에서만 설명한다.
-->
<section id="wood" class="section" aria-labelledby="wood-h">
<div class="wrap two">
<figure class="two-photo reveal">
@ -103,31 +95,26 @@
얻을 수 없고, 자연에서 쓰러진 나무에서만 길어 올립니다.
</p>
<p class="muted">
향연은 인공접종 목재를 다루지 않습니다. 매입 단계에서 수지 함량을
측정해 기록하고, 그 기록이 통과하지 못한 나무는 아카이브에 들어오지
않습니다.
이 섹션은 산지·수지 함량·숙성 상태를 함께 기록하는 방식을 설명합니다.
특정 목재의 매입 기준이나 실제 아카이브 운영 정책을 뜻하지 않습니다.
</p>
</div>
</div>
</section>
<!--
숙성 — 아카이브의 뼈대. 연도가 곧 목록이다.
카드가 아니라 장부의 행: 연도 · 입목 · 수지 · 상태.
-->
<section id="aging" class="section band" aria-labelledby="aging-h">
<div class="wrap">
<div class="aging-head">
<h2 id="aging-h">숙성 장부</h2>
<h2 id="aging-h">숙성 기록 예시</h2>
<p class="muted">
들여온 해와 수지 함량, 지금 상태입니다. 향은 재고가 아니라 연도입니다 —
같은 나무가 다시 들어오지 않습니다.
들여온 해와 수지 함량, 상태를 한 줄에서 읽는 기록 형식입니다. 아래
연도·산지·수지 함량은 실제 측정이나 재고를 뜻하지 않습니다.
</p>
</div>
<div class="table-wrap reveal">
<table>
<caption class="sr-only">
숙성 장부 — 입목 연도, 산지, 수지 함량, 현재 상태
숙성 기록 예시 — 입목 연도, 산지, 수지 함량, 상태
</caption>
<thead>
<tr>
@ -142,85 +129,80 @@
<th scope="row">2001년</th>
<td>베트남 카호아</td>
<td class="num">64.2%</td>
<td>열매 · 매진 임박</td>
<td>열매 · 상태 예시</td>
</tr>
<tr>
<th scope="row">2012년</th>
<td>인도네시아 리아우</td>
<td class="num">51.8%</td>
<td>침향 절편 · 소량</td>
<td>침향 절편 · 상태 예시</td>
</tr>
<tr>
<th scope="row">2024년</th>
<td>캄보디아 프놈펜 인근</td>
<td class="num">47.3%</td>
<td>숙성 중 · 비매품</td>
<td>숙성 중 · 상태 예시</td>
</tr>
</tbody>
</table>
</div>
<p class="aging-note muted">
수지 함량: 에탄올 용출법 실측. 열매(초험)는 기증용으로만 남겨둡니다.
수지 함량과 상태 표기는 기록 형식을 설명하기 위한 예시 값입니다.
</p>
</div>
</section>
<!--
아카이브 — 세 개의 물건. 사진 없는 목록.
종이의 물성을 믿고 글자로만 판다(R2: 텍스트가 상품).
-->
<section id="collection" class="section" aria-labelledby="collection-h">
<div class="wrap">
<h2 id="collection-h">지금 만질 수 있는 것</h2>
<h2 id="collection-h">기록으로 살펴보는 구성</h2>
<ol class="collection">
<li class="item reveal">
<div class="item-text">
<h3>자연 침향 절편</h3>
<p>2001년 입목 · 수지 64.2% · 10g</p>
</div>
<p class="item-price">180,000원</p>
<p class="item-price">예시 180,000원</p>
</li>
<li class="item reveal" style="--delay: 80ms">
<div class="item-text">
<h3>백단 침 향</h3>
<p>인도 마이소르 노목 · 20g · 연소 45분</p>
</div>
<p class="item-price">42,000원</p>
<p class="item-price">예시 42,000원</p>
</li>
<li class="item reveal" style="--delay: 160ms">
<div class="item-text">
<h3>침향 인엽</h3>
<p>리아우산 · 2012년 입목 · 3g</p>
</div>
<p class="item-price">96,000원</p>
<p class="item-price">예시 96,000원</p>
</li>
</ol>
<p class="muted collection-note">
모든 물건은 삼청동 아카이브룸에서 직접 내어드립니다. 향을 맡아 보고
정하시면 됩니다 — 예약제이며 회당 한 분만 들어갑니다.
이 목록은 상품명·단위·가격을 기록하는 방식을 보여 줍니다. 실제 상품 판매,
향 시향, 예약 또는 방문을 제공하지 않습니다.
</p>
</div>
</section>
<!-- 방문 — 마지막 섹션이 푸터 역할 -->
<section id="visit" class="section visit" aria-labelledby="visit-h">
<div class="wrap visit-grid">
<div>
<h2 id="visit-h">삼청동, 예약제</h2>
<h2 id="visit-h">방문 정보 예시</h2>
<address>
서울 종로구 삼청로 00길 00<br />
경복궁역 5번 출구에서 도보 6분
가상 아카이브 안내<br />
실제 장소와 방문 경로를 제공하지 않습니다.
</address>
</div>
<div class="visit-info">
<dl>
<div>
<dt>예약</dt>
<dt>방문 시간 예시</dt>
<dd>화–토 · 11:00, 14:00, 16:00 회당 50분</dd>
</div>
<div>
<dt>문의</dt>
<dd><a class="link" href="mailto:archive@hyang.kr">archive@hyang.kr</a></dd>
<dt>예약</dt>
<dd>실제 예약 및 문의를 받지 않습니다.</dd>
</div>
</dl>
</div>
@ -229,18 +211,5 @@
</section>
</main>
<script>
const io = new IntersectionObserver(
(entries) => {
for (const e of entries) {
if (!e.isIntersecting) continue;
e.target.classList.add("is-in");
io.unobserve(e.target);
}
},
{ rootMargin: "0px 0px -8% 0px", threshold: 0.05 },
);
for (const el of document.querySelectorAll(".reveal")) io.observe(el);
</script>
</body>
</html>

View file

@ -0,0 +1,15 @@
@font-face {
font-family: "MaruBuri";
src: url("../assets/fonts/MaruBuri-Regular.woff2") format("woff2");
font-style: normal;
font-weight: 400;
font-display: swap;
}
@font-face {
font-family: "MaruBuri";
src: url("../assets/fonts/MaruBuri-SemiBold.woff2") format("woff2");
font-style: normal;
font-weight: 600;
font-display: swap;
}

View file

@ -1,4 +1,4 @@
@import url("./tokens.css");
@import url("./tokens.css?v=20260912all");
/* ==========================================================================
향연 — quiet-luxury. 밝은 편집면 위의 기록.
@ -138,6 +138,7 @@ a {
}
.nav {
display: flex;
flex-wrap: wrap;
gap: clamp(var(--space-3), 2vw, var(--space-5));
font-size: var(--step--1);
}
@ -157,13 +158,26 @@ a {
/* ── 히어로 ───────────────────────────────────────────────────────────── */
.hero {
padding-block: var(--space-8) var(--space-7);
display: grid;
align-items: center;
min-block-size: min(46rem, calc(100svh - 4rem));
padding-block: var(--space-6);
border-bottom: 1px solid var(--line);
}
.hero-grid {
display: grid;
grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
gap: var(--space-7) var(--space-6);
align-items: start;
grid-template-columns: minmax(0, 1fr) minmax(16rem, 28rem);
gap: clamp(var(--space-5), 6vw, var(--space-8));
align-items: center;
}
.hero-grid > *,
.two > *,
.visit-grid > * {
min-width: 0;
}
.hero-text {
max-width: 38rem;
padding-block: var(--space-4);
}
.hero-kicker {
font-size: var(--step--1);
@ -172,7 +186,7 @@ a {
margin-bottom: var(--space-5);
}
.lead {
max-width: 30em;
max-width: 42rem;
}
.hero-text .lead {
margin-top: var(--space-5);
@ -181,7 +195,9 @@ a {
margin-top: var(--space-6);
}
.btn {
display: inline-block;
display: inline-flex;
align-items: center;
min-height: 44px;
border: 1px solid var(--ink);
color: var(--ink);
text-decoration: none;
@ -196,22 +212,24 @@ a {
}
.hero-photo {
position: relative;
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: var(--space-2);
justify-content: end;
width: min(100%, 28rem);
justify-self: end;
align-items: start;
}
.hero-photo img {
display: block;
width: 100%;
aspect-ratio: 3 / 4;
height: auto;
object-fit: cover;
border-radius: var(--radius);
}
.hero-photo figcaption {
font-size: var(--step--1);
color: var(--ink-faint);
color: var(--ink-muted);
letter-spacing: 0.06em;
}
@ -220,6 +238,12 @@ a {
.section {
padding-block: var(--space-8);
}
.section[id] {
scroll-margin-top: 6rem;
}
#aging .wrap {
width: min(100% - 2 * var(--pad-inline), var(--container-wide));
}
.band {
background: var(--surface-raised);
border-block: 1px solid var(--line);
@ -247,6 +271,9 @@ a {
.two-text p + p {
margin-top: var(--space-4);
}
.two-text p {
max-width: 42rem;
}
.two-text .muted {
margin-top: var(--space-4);
}
@ -254,7 +281,7 @@ a {
/* ── 숙성 장부 ────────────────────────────────────────────────────────── */
.aging-head {
max-width: 36em;
max-width: 42rem;
margin-bottom: var(--space-5);
}
.aging-head p {
@ -305,8 +332,8 @@ tbody th {
border-top: 1px solid var(--line);
}
.item {
display: flex;
justify-content: space-between;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: baseline;
gap: var(--space-4);
padding-block: var(--space-4);
@ -323,11 +350,12 @@ tbody th {
.item-price {
font-family: var(--font-display);
font-size: var(--step-1);
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.collection-note {
margin-top: var(--space-4);
max-width: 34em;
max-width: 42rem;
}
/* ── 방문 ─────────────────────────────────────────────────────────────── */
@ -368,23 +396,9 @@ tbody th {
/* ── 등장 모션 — 다른 페이지보다 느리다 ──────────────────────────────── */
.reveal {
opacity: 0;
translate: 0 16px;
transition: opacity var(--dur-normal) var(--ease-soft),
translate var(--dur-normal) var(--ease-soft);
transition-delay: var(--delay, 0ms);
}
.reveal.is-in {
opacity: 1;
translate: 0 0;
}
@media (prefers-reduced-motion: reduce) {
.reveal {
opacity: 1;
translate: 0 0;
transition: none;
}
}
/* ── 반응형 ───────────────────────────────────────────────────────────── */
@ -397,6 +411,7 @@ tbody th {
.hero-photo {
max-width: 24rem;
justify-content: start;
justify-self: start;
}
.visit-grid {
grid-template-columns: 1fr;
@ -405,6 +420,7 @@ tbody th {
display: none;
}
.hero {
min-block-size: auto;
padding-block: var(--space-6);
}
}
@ -416,11 +432,13 @@ tbody th {
}
.nav {
width: 100%;
justify-content: space-between;
justify-content: flex-start;
}
.item {
flex-direction: column;
align-items: flex-start;
grid-template-columns: 1fr;
gap: var(--space-1);
}
.item-price {
white-space: normal;
}
}

View file

@ -28,7 +28,7 @@
--surface-raised: #efe9dc;
--ink: #2b2724;
--ink-muted: #6d655c;
--ink-faint: #9a9083;
--ink-faint: #746c60;
--line: #ddd4c3;
--accent: #7c3b24; /* 적갈 — 링크와 핵심 동작만 */
--accent-hover: #5f2c1a;
@ -44,8 +44,10 @@
--radius: 2px;
--container: 1040px;
--pad-inline: clamp(1.25rem, 1rem + 2.5vw, 4rem);
/* 향의 문장은 좁게, 장부와 실물 기록은 넓게 둔다. */
--container: 1160px;
--container-wide: 1440px;
--pad-inline: clamp(1.25rem, 1rem + 2.5vw, 4.5rem);
--dur-quick: 160ms;
--dur-normal: 560ms; /* 느린 페이지 — 등장도 느리게 */

View file

@ -0,0 +1,27 @@
# Lumina Optics design.md
## 브리프
- 무엇을: IOR와 분산을 조작하는 가상 광학 시험대.
- 누구에게: 광선 모델의 조건과 결과물을 함께 읽으려는 방문자.
- 제약: WebGL/정적 폴백, 기존 슬라이더와 가상·측정 아님 표기를 유지한다.
## 결정표
| 축 | 관찰과 적용 범위 | 결정·역할 토큰 | 기각 조건 |
|---|---|---|---|
| 구도 | 기준 1440 화면에서 큰 프리즘이 제목과 조작을 폴드 아래로 밀었다. | 제목·한계 설명을 상단에 두고 `9:4` 캔버스/제어 프레임을 배치한다. `--container`, `--space-4`, `--space-6`. | 캔버스가 제어보다 큰 면적을 차지해 둘 중 하나가 첫 화면에서 사라지면 재조정한다. |
| 타입 | 제목은 모델의 의미, 모노는 조건과 조작값을 읽게 해야 한다. | `--step-3` 제목, `--step--1` 시험대 원장과 제어 보조문을 유지한다. | 실제 긴 라벨이 320px 또는 200% 확대에서 제어를 밀면 폭·간격을 수정한다. |
| 색 | 스펙트럼만 광학 결과를 나타내며 UI 색은 보조여야 한다. | `--surface`, `--surface-raised`, `--accent-stroke`을 프레임·포커스·입력에만 쓴다. | UI 색이 캔버스의 분산 광선보다 먼저 읽히면 채도를 줄인다. |
## 근거와 경계
[art-direction.md](../../../../../packages/skill/references/art-direction.md)의 Hofmann 관련 구도·군집 관찰을 제목/작업면 분리에만 적용했다. [typography.md](../../../../../packages/skill/references/typography.md)의 Ruder 관련 실제 카피·수치 proof 범위를 타입 계층에만 적용했다. [visual-design-lineage.md](../../../../../packages/skill/references/visual-design-lineage.md)의 Albers 관련 색 맥락은 캔버스와 UI 표면의 대비 확인에만 적용했다. 이 세 자료는 성능·이해도 법칙으로 사용하지 않는다.
[trustworthy-showcases.md](../../../../../packages/skill/references/trustworthy-showcases.md)의 진실 범위에 따라 모델·사양·곡선의 illustrative 표기를 가까운 공통 표면에 유지한다. 캔버스 안에는 텍스트를 굽지 않는다.
## 2026-09-12 현재 검증 기록
- `verify-final` 18개 묶음은 16개가 즉시 통과했다. 전체 묶음의 invariant·visual 검사는 Gaon·Dure를 대상으로 하며, 나머지 2개는 수정 뒤 release 독립 재실행에서 ALL PASS·ALL MATCH였다.
- root는 28개 모바일, 21개 데스크톱, 7개 하위 경로와 30개 앱 상태를 직접 검토해 수용했다. stress 112건은 hard failure 0이며, 가로 넘침 4건은 targeted fix 뒤 390px 재측정에서 0이다.
- 이 기록은 렌더·계약 범위의 현재 결과다. 모든 폭·키보드 경로·인쇄 등 개별 기능을 완전 재실행했다는 뜻이 아니며, 참여자 과업 효과도 미측정이다.

View file

@ -3,15 +3,15 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Lumina Optics · Refraction Bench</title>
<title>Lumina Optics · Interactive Optical Study</title>
<meta
name="description"
content="A WebGL bench where a single beam meets a prism: refraction and dispersion computed per frame. Drag the prism, change the index."
content="An interactive optical study of refraction and dispersion. The lens profile, curves, and specifications are illustrative."
/>
<meta property="og:title" content="Lumina Optics · Refraction Bench" />
<meta property="og:title" content="Lumina Optics · Interactive Optical Study" />
<meta
property="og:description"
content="One beam, one prism, per-frame refraction."
content="An illustrative light path you can inspect and adjust."
/>
<meta property="og:type" content="website" />
<meta name="theme-color" content="#101214" />
@ -22,7 +22,7 @@
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Sans+KR:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap"
/>
<link rel="stylesheet" href="./styles/main.css?v=2" />
<link rel="stylesheet" href="./styles/main.css?v=20260912all" />
<link
rel="icon"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23101214'/%3E%3Cpath d='M6 16h8m8 0h4M14 16l10-7v14z' fill='none' stroke='%23BFE8EE' stroke-width='1.4'/%3E%3C/svg%3E"
@ -45,40 +45,54 @@
</header>
<main id="main">
<!--
The bench. The product is light behaviour, so the hero IS the instrument:
one beam, one prism, per-frame refraction. Drag rotates; sliders change
the index and the dispersion — they are the two honest knobs a bench
has. Everything degrades to a static photograph.
-->
<section id="bench" class="hero" aria-labelledby="hero-h">
<div class="bench-stage reveal">
<canvas
id="gl"
aria-label="Interactive prism: a white beam enters from the left and exits as a spectrum. Drag to rotate the prism."
></canvas>
<img
class="bench-fallback"
src="./assets/prism-bench.webp"
alt="A glass prism on a dark optical breadboard splitting one collimated beam into a spectrum"
width="1440"
height="810"
/>
</div>
<div class="wrap hero-inner">
<div class="wrap hero-summary">
<div class="hero-text">
<p class="hero-kicker reveal">Refraction bench · WebGL</p>
<p class="hero-kicker reveal">Interactive optical study</p>
<h1 id="hero-h" class="reveal" style="--delay: 60ms">
One beam,<br />bent honestly
</h1>
<p class="lead reveal" style="--delay: 120ms">
No photograph on this screen is doing the work — the prism is
being refracted right now, per frame, on your GPU. Drag it. Change
the index. The spectrum you see is the same arithmetic lens
designers use, minus the glasses.
Move the prism to compare how refractive index and dispersion
redirect one beam. This view illustrates an optical model; it is
not a measured photograph, a product specification, or a lab result.
</p>
</div>
<div class="hud reveal" style="--delay: 160ms">
</div>
<div class="wrap bench-layout">
<div class="bench-shell">
<div class="bench-stage reveal">
<canvas
id="gl"
aria-label="Illustrative interactive prism: a white beam enters from the left and exits as a spectrum. Drag to rotate the concept model."
></canvas>
<img
class="bench-fallback"
src="./assets/prism-bench.webp"
alt="Illustrative glass prism on a dark optical surface splitting one collimated beam into a spectrum"
width="1440"
height="810"
/>
</div>
</div>
<aside class="bench-controls">
<div class="bench-ledger" role="group" aria-label="Bench configuration">
<dl>
<div>
<dt>illustrative beam reference</dt>
<dd>632.8 nm · collimated</dd>
</div>
<div>
<dt>variable</dt>
<dd>index + dispersion</dd>
</div>
<div>
<dt>view</dt>
<dd>interactive light study · static reference</dd>
</div>
</dl>
</div>
<div class="hud reveal" style="--delay: 160ms">
<div class="hud-row">
<div class="hud-row-head"><label for="ior">Index of refraction</label><output id="ior-out" for="ior">1.52</output></div>
<input type="range" id="ior" min="1.45" max="2.20" step="0.01" value="1.52" />
@ -87,56 +101,48 @@
<div class="hud-row-head"><label for="disp">Dispersion (Abbe ↓)</label><output id="disp-out" for="disp">0.18</output></div>
<input type="range" id="disp" min="0" max="0.6" step="0.01" value="0.18" />
</div>
<p class="hud-note" id="hud-note">Drag the prism to rotate · beam is collimated at 632.8 nm reference</p>
</div>
<p class="hud-note" id="hud-note">Drag the prism to rotate · illustrative light path at a 632.8 nm reference</p>
</div>
</aside>
</div>
</section>
<!--
Anatomy — cut lens photography. Real glass, not renders.
-->
<section id="anatomy" class="section" aria-labelledby="anatomy-h">
<div class="wrap two">
<figure class="two-photo reveal">
<img
src="./assets/lens-section.webp"
alt="Cutaway of a precision lens: a stack of glass elements in a metal barrel, edge-lit so each surface shows a refraction line"
alt="Illustrative lens cutaway: a stack of glass elements in a metal barrel, edge-lit to show refraction paths"
width="900"
height="1200"
loading="lazy"
/>
<figcaption>L-85 cutaway · 7 elements in 6 groups</figcaption>
<figcaption>L-85 concept cutaway · illustrative element layout</figcaption>
</figure>
<div class="two-text">
<h2 id="anatomy-h">Glass decides<br />before software does</h2>
<h2 id="anatomy-h">Glass sets<br />the light path</h2>
<p>
A lens is a chain of refractions, and every surface answers before
any firmware runs. Our L-85 uses two low-dispersion elements to
keep the spectrum you just dragged from fanning out across the
frame — dispersion corrected in glass, not in a profile.
A lens is a chain of refractions: every surface bends the path
before an image is made. The L-85 concept uses low-dispersion
elements to explain why a design can keep colours from spreading
across the frame.
</p>
<p class="muted">
The cutaway above is a physical section, milled for measurement.
Element count and spacing are measured from it, not from the
drawing.
The cutaway is a constructed reference image. Its element count
and spacing are illustrative, not a measurement of a commercial lens.
</p>
</div>
</div>
</section>
<!--
MTF — the only curve that argues. Static, thin, honest:
measured at infinity, full aperture, both field directions.
-->
<section id="mtf" class="section band" aria-labelledby="mtf-h">
<div class="wrap mtf-grid">
<div class="mtf-text">
<h2 id="mtf-h">MTF, not adjectives</h2>
<h2 id="mtf-h">MTF, read with conditions</h2>
<p>
“Tack sharp” is not a specification. Contrast transfer at given
line pairs per millimetre is. These curves are measured on an
optical bench at full aperture — sagittal and meridional, out to
the corner.
Contrast transfer at stated line pairs per millimetre gives a
clearer comparison than a slogan. These curves illustrate the
labels a measured chart needs; they are not optical-bench results.
</p>
<ul class="mtf-legend">
<li><span class="key key-solid" aria-hidden="true"></span>10 lp/mm · sagittal</li>
@ -146,7 +152,7 @@
</ul>
</div>
<figure class="mtf-plot reveal">
<svg viewBox="0 0 560 320" role="img" aria-label="MTF curves: contrast stays above 0.85 at 10 lp/mm across the field; at 30 lp/mm it falls from 0.75 at center to 0.42 at the corner.">
<svg viewBox="0 0 560 320" role="img" aria-label="Illustrative MTF curves showing how contrast can be compared from the centre to the corner; this is not a measured result.">
<g class="chart-grid">
<line x1="48" y1="16" x2="48" y2="272" />
<line x1="48" y1="272" x2="536" y2="272" />
@ -166,76 +172,62 @@
<text x="292" y="292" text-anchor="middle">image field (mm)</text>
<text x="536" y="292" text-anchor="end">corner 21.6</text>
</g>
<!-- 10 lp/mm sagittal: high, gentle fall -->
<polyline class="mtf mtf-10s" points="48,32 172,36 296,44 420,60 536,76" />
<!-- 10 lp/mm meridional -->
<polyline class="mtf mtf-10m" points="48,34 172,40 296,52 420,72 536,96" />
<!-- 30 lp/mm sagittal -->
<polyline class="mtf mtf-30s" points="48,84 172,96 296,120 420,160 536,196" />
<!-- 30 lp/mm meridional -->
<polyline class="mtf mtf-30m" points="48,90 172,108 296,140 420,186 536,232" />
</svg>
<figcaption>L-85 · f/1.8 · infinity focus · bench-measured 2026-07</figcaption>
<figcaption>L-85 concept · f/1.8 · infinity focus · illustrative curve</figcaption>
</figure>
</div>
</section>
<!--
Spec — with conditions.
-->
<section class="section" aria-labelledby="spec-h">
<div class="wrap">
<h2 id="spec-h" class="reveal">Specification</h2>
<h2 id="spec-h" class="reveal">Reference specification</h2>
<div class="table-wrap reveal" style="--delay: 80ms">
<table>
<caption class="sr-only">L-85 specification — item, value, condition</caption>
<caption class="sr-only">L-85 concept specification — illustrative item, value, and condition</caption>
<thead>
<tr>
<th scope="col">Item</th>
<th scope="col" class="num">Value</th>
<th scope="col" class="num">Illustrative value</th>
<th scope="col">Condition</th>
</tr>
</thead>
<tbody>
<tr><th scope="row">Focal length</th><td class="num">85 mm</td><td>nominal · ±1.5% at infinity</td></tr>
<tr><th scope="row">Aperture</th><td class="num">f/1.8 – f/16</td><td>9-blade · near-circular</td></tr>
<tr><th scope="row">Elements</th><td class="num">7 in 6</td><td>2 LD · measured from cutaway</td></tr>
<tr><th scope="row">Close focus</th><td class="num">0.62 m</td><td>1:7.2 magnification</td></tr>
<tr><th scope="row">Filter</th><td class="num">Ø 62 mm</td><td>brass, front-threaded</td></tr>
<tr><th scope="row">Weight</th><td class="num">438 g</td><td>without caps, with hood</td></tr>
<tr><th scope="row">Focal length</th><td class="num">85 mm</td><td>concept reference</td></tr>
<tr><th scope="row">Aperture</th><td class="num">f/1.8 – f/16</td><td>concept reference</td></tr>
<tr><th scope="row">Elements</th><td class="num">7 in 6</td><td>illustrative layout</td></tr>
<tr><th scope="row">Close focus</th><td class="num">0.62 m</td><td>illustrative configuration</td></tr>
<tr><th scope="row">Filter</th><td class="num">Ø 62 mm</td><td>illustrative configuration</td></tr>
<tr><th scope="row">Weight</th><td class="num">438 g</td><td>illustrative configuration</td></tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- Contact -->
<section id="contact" class="section band contact" aria-labelledby="contact-h">
<div class="wrap contact-grid">
<h2 id="contact-h">Bench visits</h2>
<h2 id="contact-h">About this study</h2>
<p>
We measure loaner samples for free and publish the curves —
manufacturers welcome or not. Bench time and MTF requests:
This is a concept page for reading an optical model. It does not
schedule sample loans, measurements, or MTF requests.
</p>
<a class="btn" href="mailto:bench@lumina.optics">bench@lumina.optics</a>
<a class="btn" href="#bench">Return to the light study</a>
</div>
</section>
</main>
<footer class="site-foot">
<div class="wrap foot-inner">
<p>lumina optics</p>
<p class="foot-meta">measurements reproducible on request · bench cal. 2026-06</p>
<p>lumina optics · optical study</p>
<p class="foot-meta">illustrative model · no commercial measurement service</p>
</div>
</footer>
<script type="module">
/*
Refraction bench — three.js, one prism, two honest uniforms.
Budget note: three.module.js ≈ 165KB gzip. 예산 상향 — 이 페이지의
제품이 WebGL 자체다(데모 브리프 예산 상향 허용).
폴백: WebGL 실패 시 사진이 보이고 캔버스는 뜨지 않는다(이미 마크업에 있음).
*/
const canvas = document.getElementById("gl");
const fallback = document.querySelector(".bench-fallback");
const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
@ -243,32 +235,30 @@
let gl = null;
try {
gl = canvas.getContext("webgl2", { antialias: true, alpha: true });
} catch (e) { /* fall through */ }
} catch {}
if (!gl) {
if (!gl || reduced) {
canvas.remove();
document.getElementById("hud-note").textContent =
"WebGL unavailable — showing the bench photograph instead.";
reduced
? "Motion reduced — showing a static reference image instead."
: "Interactive view unavailable — showing a static reference image instead.";
} else {
fallback.remove();
const THREE = await import("https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js");
const renderer = new THREE.WebGLRenderer({ canvas, context: gl, antialias: true, alpha: true });
renderer.setClearColor(0x000000, 0);
const DPR = Math.min(devicePixelRatio || 1, 1.5);
renderer.setPixelRatio(DPR);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(32, 1, 0.1, 50);
camera.position.set(0, 0, 9);
// ----- prism: triangular cross-section (CylinderGeometry, 3 segments)
const prismGeo = new THREE.CylinderGeometry(2.1, 2.1, 2.6, 3, 1, false);
prismGeo.rotateX(Math.PI / 2); // axis toward camera
prismGeo.rotateZ(Math.PI / 2); // flat edge up to the beam
prismGeo.rotateX(Math.PI / 2);
prismGeo.rotateZ(Math.PI / 2);
const uniforms = {
uTime: { value: 0 },
uIor: { value: 1.52 },
uDisp: { value: 0.18 },
};
@ -276,7 +266,7 @@
const prismMat = new THREE.ShaderMaterial({
uniforms,
transparent: true,
vertexShader: /* glsl */`
vertexShader: `
varying vec3 vN;
varying vec3 vView;
void main() {
@ -285,44 +275,31 @@
vView = normalize(-mv.xyz);
gl_Position = projectionMatrix * mv;
}`,
fragmentShader: /* glsl */`
fragmentShader: `
precision highp float;
varying vec3 vN;
varying vec3 vView;
uniform float uIor;
uniform float uDisp;
// wavelength ramp: violet → cyan → yellow → red
vec3 spectrum(float t) {
t = clamp(t, 0.0, 1.0);
return clamp(
vec3(
smoothstep(0.30, 0.00, t) + smoothstep(0.70, 1.00, t),
smoothstep(0.00, 0.35, t) - smoothstep(0.60, 1.00, t),
smoothstep(0.00, 0.30, t) - smoothstep(0.45, 0.75, t)
), 0.0, 1.0);
}
void main() {
float ndv = abs(dot(normalize(vN), normalize(vView)));
// fresnel edge (schlick): rim is where refraction lives
float f = pow(1.0 - ndv, 3.0);
// refraction deflection grows with index
float bend = (uIor - 1.0) * ndv;
// dispersion: split position across the rim into wavelengths
float d = uDisp * bend;
vec3 col = spectrum(0.5 + (1.0 - ndv) * (0.6 + d * 4.0));
// glass body: near-black with faint interior glow
vec3 body = vec3(0.02, 0.025, 0.03) * (0.4 + ndv);
vec3 rim = col * f * 1.35;
float alpha = clamp(f * 0.95 + 0.06, 0.0, 1.0);
vec3 body = vec3(0.035, 0.082, 0.094) * (0.7 + ndv * 0.65);
vec3 rim = vec3(0.22, 0.70, 0.75) * (0.24 + f * 0.92);
float alpha = clamp(0.18 + f * 0.62, 0.0, 0.82);
gl_FragColor = vec4(body + rim, alpha);
}`,
side: THREE.DoubleSide,
});
const prism = new THREE.Mesh(prismGeo, prismMat);
scene.add(prism);
const prismEdges = new THREE.LineSegments(
new THREE.EdgesGeometry(prismGeo, 8),
new THREE.LineBasicMaterial({ color: 0xbfe8ee, transparent: true, opacity: 0.78 }),
);
prismEdges.renderOrder = 2;
const prismGroup = new THREE.Group();
prismGroup.add(prism, prismEdges);
scene.add(prismGroup);
// ----- beam: incoming line + dispersed fan (additive planes)
const beamGroup = new THREE.Group();
const mkBeam = (w, h, color1, color2, op) => {
const m = new THREE.Mesh(
@ -345,24 +322,35 @@
);
return m;
};
const beamIn = mkBeam(9, 0.10, 0xdfe8ec, 0xffffff, 0.55);
beamIn.position.set(0, 1.45, -0.2);
const beamIn = mkBeam(4.1, 0.12, 0xdfe8ec, 0xffffff, 0.92);
beamIn.position.set(-3.05, 0, -0.34);
beamGroup.add(beamIn);
const fan = mkBeam(9, 1.7, 0x8fd8e8, 0xf2b060, 0.30);
fan.position.set(-0.4, 0.35, -0.25);
fan.rotation.z = 0.085;
beamGroup.add(fan);
const spectrumRays = [0x7379e8, 0x62c9e8, 0x89dbaa, 0xf0da72, 0xee9866].map((color) => {
const ray = mkBeam(3.9, 0.075, color, color, 0.92);
beamGroup.add(ray);
return ray;
});
scene.add(beamGroup);
// ----- interaction
let dragging = false, lastX = 0, vel = 0.0016, rot = 0;
function updateSpectrum() {
const spread = 0.11 + uniforms.uDisp.value * 0.56;
const bend = (uniforms.uIor.value - 1.45) * 0.26;
spectrumRays.forEach((ray, i) => {
const offset = i / (spectrumRays.length - 1) - 0.5;
ray.position.set(2.75, offset * spread * 2.4 + bend, -0.38);
ray.rotation.z = offset * spread * 0.76;
});
}
updateSpectrum();
let dragging = false, lastX = 0, rot = -0.08;
canvas.addEventListener("pointerdown", (e) => { dragging = true; lastX = e.clientX; canvas.setPointerCapture(e.pointerId); });
canvas.addEventListener("pointermove", (e) => {
if (!dragging) return;
const dx = e.clientX - lastX;
lastX = e.clientX;
rot += dx * 0.006;
vel = dx * 0.0006;
renderFrame();
});
const end = () => (dragging = false);
canvas.addEventListener("pointerup", end);
@ -377,38 +365,32 @@
el.addEventListener("input", () => {
uni.value = parseFloat(el.value);
out.textContent = fmt(uni.value);
updateSpectrum();
renderFrame();
});
bind(iorEl, iorOut, uniforms.uIor, (v) => v.toFixed(2));
bind(dispEl, dispOut, uniforms.uDisp, (v) => v.toFixed(2));
// ----- sizing
const stage = canvas.parentElement;
function fit() {
const r = stage.getBoundingClientRect();
const h = Math.max(320, Math.min(r.height, 760));
const aspect = r.width / h;
renderer.setPixelRatio(Math.min(devicePixelRatio || 1, aspect < 1.25 ? 1.25 : 1.5));
renderer.setSize(r.width, h, false);
camera.aspect = r.width / h;
camera.aspect = aspect;
camera.position.z = aspect < 1.25 ? 15.5 : 9;
camera.updateProjectionMatrix();
renderFrame();
}
fit();
new ResizeObserver(fit).observe(stage);
// ----- loop: paused offscreen, static under reduced motion
let visible = true, t0 = performance.now();
const io = new IntersectionObserver((en) => { visible = en[0].isIntersecting; });
io.observe(canvas);
function frame() {
if (!visible) { requestAnimationFrame(frame); return; }
const t = (performance.now() - t0) / 1000;
uniforms.uTime.value = t;
if (!dragging) { vel *= 0.98; rot += Math.max(0.0016, Math.abs(vel)) * Math.sign(vel || 1); }
prism.rotation.z = rot;
beamGroup.rotation.z = Math.sin(t * 0.4) * 0.01;
function renderFrame() {
prismGroup.rotation.z = rot;
renderer.render(scene, camera);
if (!reduced) requestAnimationFrame(frame);
}
frame(); // reduced-motion: renders exactly one static frame
renderFrame();
}
const io2 = new IntersectionObserver(

View file

@ -1,4 +1,4 @@
@import url("./tokens.css");
@import url("./tokens.css?v=20260912all");
/* ==========================================================================
Lumina Optics — 광학 시험대. 색은 캔버스 안에서만.
@ -145,15 +145,63 @@ a {
R2 원칙(design.md): 3D 위에 본문을 얹지 않는다. ─────────────────────── */
.hero {
padding-block: var(--space-4);
border-bottom: 1px solid var(--line);
}
.hero-summary {
margin-bottom: var(--space-4);
}
.bench-layout {
display: grid;
grid-template-columns: minmax(0, 9fr) minmax(17rem, 4fr);
gap: var(--space-4);
align-items: stretch;
}
.bench-shell {
min-width: 0;
background:
radial-gradient(72% 90% at 50% 22%, rgba(127, 185, 196, 0.06), transparent 66%),
var(--surface);
}
.bench-stage {
position: relative;
height: clamp(320px, 56vh, 560px);
height: clamp(20rem, 28vw, 22rem);
background:
radial-gradient(80% 90% at 50% 30%, rgba(127, 185, 196, 0.05), transparent 60%),
radial-gradient(70% 88% at 50% 48%, rgba(127, 185, 196, 0.10), transparent 68%),
var(--surface);
border-bottom: 1px solid var(--line);
border: 1px solid var(--line-strong);
border-radius: var(--radius);
overflow: hidden;
}
.bench-ledger {
background: var(--surface-raised);
border: 1px solid var(--line-strong);
border-radius: var(--radius);
}
.bench-ledger dl {
display: grid;
grid-template-columns: 1fr;
}
.bench-ledger dl > div {
min-width: 0;
padding: 0.85rem var(--space-3) 0.95rem;
border-top: 1px solid var(--line);
}
.bench-ledger dl > div:first-child {
border-top: 0;
}
.bench-ledger dt,
.bench-ledger dd {
font-family: var(--font-mono);
font-size: 0.6875rem;
letter-spacing: 0.045em;
}
.bench-ledger dt {
color: var(--ink-faint);
}
.bench-ledger dd {
margin-top: 0.2rem;
color: var(--ink-muted);
}
#gl,
.bench-fallback {
@ -169,13 +217,6 @@ a {
#gl:active {
cursor: grabbing;
}
.hero-inner {
display: grid;
grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
gap: var(--space-5) var(--space-7);
align-items: start;
padding-block: var(--space-6);
}
.hero-kicker {
font-family: var(--font-mono);
font-size: var(--step--1);
@ -187,8 +228,8 @@ a {
margin-top: var(--space-3);
}
.hud {
background: color-mix(in srgb, var(--surface) 78%, transparent);
backdrop-filter: blur(8px);
margin-top: var(--space-3);
background: var(--surface-raised);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: var(--space-4);
@ -392,7 +433,9 @@ tbody td:not(.num) {
max-width: 34em;
}
.btn {
display: inline-block;
display: inline-flex;
align-items: center;
min-height: 44px;
border: 1px solid var(--accent-stroke);
color: var(--accent);
text-decoration: none;
@ -451,13 +494,11 @@ tbody td:not(.num) {
/* ── 반응형 ───────────────────────────────────────────────────────────── */
@media (max-width: 900px) {
.hero-inner {
position: static;
.bench-layout {
grid-template-columns: 1fr;
padding-block: var(--space-5);
}
.bench-stage {
height: clamp(300px, 48vh, 460px);
height: clamp(320px, 82vw, 540px);
}
.two,
.mtf-grid,
@ -469,6 +510,20 @@ tbody td:not(.num) {
padding-block: var(--space-6);
}
}
@media (max-width: 640px) {
.bench-ledger dl {
grid-template-columns: 1fr;
}
.bench-ledger dl > div,
.bench-ledger dl > div:first-child {
padding-inline: var(--space-3);
border-left: 0;
border-top: 1px solid var(--line);
}
.bench-ledger dl > div:first-child {
border-top: 0;
}
}
@media (max-width: 560px) {
.head-inner {
flex-direction: column;
@ -477,6 +532,9 @@ tbody td:not(.num) {
}
.nav {
width: 100%;
justify-content: space-between;
flex-wrap: wrap;
justify-content: flex-start;
column-gap: var(--space-4);
row-gap: var(--space-2);
}
}

View file

@ -31,7 +31,7 @@
--surface-raised: #15181b;
--ink: #e8ecee;
--ink-muted: #9ba4a9;
--ink-faint: #6b7479;
--ink-faint: #849096;
--line: #23272b;
--line-strong: #394046;
--accent: #bfe8ee; /* 레이저 라인(밝게 — 텍스트용) */
@ -48,8 +48,8 @@
--radius: 2px;
--container: 1120px;
--pad-inline: clamp(1.25rem, 1rem + 2.5vw, 4rem);
--container: 1240px;
--pad-inline: clamp(1.25rem, 1rem + 2.5vw, 5rem);
--dur-quick: 160ms;
--dur-normal: 420ms;

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,13 @@
# MaruBuri source record
Downloaded 2026-09-12 from the official NAVER Hangeul static host. The files are retained unchanged: no conversion or subsetting was performed.
| File | Official URL | HTTP content type | Size | SHA-256 |
| --- | --- | --- | --- | --- |
| `MaruBuri-Regular.woff2` | https://hangeul.pstatic.net/hangeul_static/webfont/MaruBuri/MaruBuri-Regular.woff2 | `font/woff2` | 433776 bytes | `4CF1341CF2F23FB3E263712DFDE1D8F25EEDCC328B696A2E5A2C8ADD55E5C17B` |
| `MaruBuri-SemiBold.woff2` | https://hangeul.pstatic.net/hangeul_static/webfont/MaruBuri/MaruBuri-SemiBold.woff2 | `font/woff2` | 466108 bytes | `7AEF4862FA3219B0A7DEB6746E27116A38C4069F4424EB51EDF103B29795F5C2` |
- Official stylesheet checked: https://hangeul.pstatic.net/hangeul_static/css/maru-buri.css
- Official NAVER license page saved verbatim as `LICENSE.html`: https://help.naver.com/service/11029/contents/18088?lang=ko&osType=PC
- `LICENSE.html` SHA-256: `8538F7921744D5E040F603BC301465EFBD038F1054D0754B7508DE3286452EC3`
- The saved official page includes the NAVER copyright notice and the SIL Open Font License 1.1 text for MaruBuri.

View file

@ -0,0 +1,17 @@
# 마테리아 스튜디오 — 디자인 결정 기록
## 관찰과 방향
- 관찰: 기존 hero는 텍스트와 장식 프레임의 비중이 커서 공간 사진이 건축 포트폴리오의 첫 증거가 되지 못했다.
- 원리 적용 범위: 사진은 공간의 빛·재료를 먼저 보이고 제목과 짧은 설명은 같은 시작선에서 범위를 설명한다. 프로젝트 목록은 대표 사례 하나와 보조 사례 둘을 구분하며, 물성은 같은 항목 위계로 비교한다.
- 결정: hero를 5:7 사진 우세로, 대표 프로젝트를 큰 카드로 조정하고 캡션을 사진의 정렬선에 맞췄다. 브론즈·따뜻한 종이 토큰과 Day/Night, 24개 상세 갤러리·라이트박스·legacy 경로를 유지한다.
- 기각 조건: 사진 크롭이 공간의 주피사체를 잃거나 Day/Night에서 캡션·필터·상태 대비가 약해지면 크롭과 표면 조합을 다시 조정한다.
## 토큰과 검증
- 타입: 로컬 `MaruBuri` 400·600은 제목·물성명, Pretendard는 본문, 모노는 메타데이터 역할을 유지한다.
- 색: `--surface #f5f3ef`, 브론즈 `--accent #76583d`, 나이트 토큰의 대비를 유지한다.
- 구도: hero 5:7, 사진 7:5, 대표 프로젝트는 넓은 화면에서 2행을 차지하며 860px 이하는 한 열이다.
- `verify-final` 18개 묶음은 16개가 즉시 통과했다. 전체 묶음의 invariant·visual 검사는 Gaon·Dure를 대상으로 하며, 나머지 2개는 수정 뒤 release 독립 재실행에서 ALL PASS·ALL MATCH였다.
- root는 28개 모바일, 21개 데스크톱, 7개 하위 경로와 30개 앱 상태를 직접 검토해 수용했다. stress 112건은 hard failure 0이며, 가로 넘침 4건은 targeted fix 뒤 390px 재측정에서 0이다.
- 이 기록은 렌더·계약 범위의 현재 결과다. 모든 폭·키보드 경로·인쇄 등 개별 기능을 완전 재실행했다는 뜻이 아니며, 참여자 과업 효과도 미측정이다.

View file

@ -6,7 +6,9 @@
<meta http-equiv="refresh" content="0; url=./projects/hannam-duplex/index.html" />
<title>한남 듀플렉스 펜트하우스 — 이동 중</title>
<link rel="canonical" href="./projects/hannam-duplex/index.html" />
<link rel="stylesheet" href="./styles/tokens.css" />
<link rel="stylesheet" href="./styles/fonts.css?v=20260912all" />
<link rel="stylesheet" href="./styles/tokens.css?v=20260912all" />
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23F5F3EF'/%3E%3Crect x='6' y='6' width='20' height='20' stroke='%23161514' stroke-width='2' fill='none'/%3E%3Crect x='12' y='12' width='8' height='8' fill='%239E8062'/%3E%3C/svg%3E" />
<style>
body {
margin: 0;

View file

@ -16,10 +16,7 @@
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/webfontworld/naver/MaruBuri.css"
/>
<link rel="stylesheet" href="./styles/fonts.css?v=20260912all" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css"
@ -28,7 +25,7 @@
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap"
/>
<link rel="stylesheet" href="./styles/main.css?v=1" />
<link rel="stylesheet" href="./styles/main.css?v=20260912all" />
<link
rel="icon"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23F5F3EF'/%3E%3Crect x='6' y='6' width='20' height='20' stroke='%23161514' stroke-width='2' fill='none'/%3E%3Crect x='12' y='12' width='8' height='8' fill='%239E8062'/%3E%3C/svg%3E"
@ -64,7 +61,7 @@
</div>
<a href="#contact" class="btn">
상담 신청
상담 항목 보기
</a>
</div>
</div>
@ -79,7 +76,7 @@
<span>하이엔드 공간 인테리어</span>
</div>
<h1 id="heroHeading" class="hero-title">
빛과 트래버틴이 빚어내는<br />
빛과 돌이 빚는<br />
침묵의 <span class="italic">공간 건축</span>
</h1>
<p class="hero-desc">
@ -96,22 +93,20 @@
</div>
</div>
<div class="hero-visual" style="position: relative;">
<div class="bezel-outer">
<div class="hero-frame">
<img
src="./assets/penthouse-hero.webp"
alt="트래버틴 벽체와 훈증 오크 수납장이 조화된 하이엔드 펜트하우스 거실 전경"
width="1200"
height="1600"
loading="eager"
/>
</div>
</div>
<div class="hero-caption-pill">
<div class="hero-caption-name">한남 펜트하우스 레지던스</div>
<div class="hero-caption-meta">Hannam · 284㎡ (86평) · 2026.04 준공</div>
</div>
<div class="hero-visual">
<figure class="hero-frame">
<img
src="./assets/penthouse-hero.webp"
alt="트래버틴 벽체와 훈증 오크 수납장이 있는 주거 공간 전경"
width="1200"
height="1600"
loading="eager"
/>
<figcaption class="hero-caption-pill">
<span class="hero-caption-name">한남 펜트하우스 레지던스</span>
<span class="hero-caption-meta">공간 사진과 물성 조합을 보여 주는 프로젝트 예시</span>
</figcaption>
</figure>
</div>
</div>
</section>
@ -272,12 +267,12 @@
<div class="process-card">
<div class="process-step">PHASE 02</div>
<h3>촉각적 머티리얼 페어링</h3>
<p>실제 1:1 자재 샘플 보드를 제작하여 거주자의 라이프스타일과 촉감 선호도를 일치시킵니다.</p>
<p>맞춤형 자재 샘플 보드로 생활 방식과 촉감 선호도를 함께 살펴봅니다.</p>
</div>
<div class="process-card">
<div class="process-step">PHASE 03</div>
<h3>3D 광학 시뮬레이션</h3>
<p>주광(Daylight)과 2700K 인공 조명의 조도 분포를 가상 렌더링으로 정밀 검증합니다.</p>
<p>낮빛과 2700K 조명에서 빛이 퍼지는 모습을 공간 시뮬레이션으로 미리 살펴봅니다.</p>
</div>
<div class="process-card">
<div class="process-step">PHASE 04</div>
@ -295,12 +290,13 @@
<div style="text-align: center; margin-bottom: var(--space-6);">
<div class="eyebrow">프라이빗 컨설팅</div>
<h2 id="contactHeading" style="font-family: var(--font-serif); font-size: var(--step-2); margin-top: 6px;">
프라이빗 공간 컨설팅 신청
프라이빗 공간 상담 항목
</h2>
<p style="font-size: var(--step-0); color: var(--ink-muted); margin-top: 6px;">
월 최대 3건의 프로젝트만을 엄선하여 타협 없는 완성도를 보증합니다.
공간 유형과 규모, 예산 범위를 정리해 보세요.
</p>
</div>
<p class="demo-notice" role="note">안내: 이 페이지의 프로젝트와 상담 항목은 공간 연출 예시입니다. 입력 정보는 저장·전송되지 않으며 실제 상담으로 접수되지 않습니다.</p>
<form id="consultForm" onsubmit="return false;">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
@ -315,18 +311,18 @@
</div>
<div class="form-group">
<label class="form-label" for="spaceArea">예상 전용면적 (평형)</label>
<input type="text" id="spaceArea" class="form-input" placeholder="예: 65평 (214㎡)" value="82평 (271㎡)" />
<input type="text" id="spaceArea" class="form-input" placeholder="예: 65평 (214㎡)" />
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
<div class="form-group">
<label class="form-label" for="clientName">성함</label>
<input type="text" id="clientName" class="form-input" placeholder="성함" value="김민준" required />
<input type="text" id="clientName" class="form-input" placeholder="성함" required />
</div>
<div class="form-group">
<label class="form-label" for="clientPhone">연락처</label>
<input type="tel" id="clientPhone" class="form-input" placeholder="010-0000-0000" value="010-5920-8812" required />
<input type="tel" id="clientPhone" class="form-input" placeholder="연락처" required />
</div>
</div>
@ -341,11 +337,11 @@
<div class="form-group">
<label class="form-label" for="clientNote">공간에 바라는 철학 및 특별 요청사항</label>
<textarea id="clientNote" class="form-textarea" rows="3" placeholder="추구하시는 분위기, 선호 자재, 착공 희망 시기 등을 자유롭게 적어주세요.">거실에 웅장한 트래버틴 벽체와 자연스러운 간접 조명을 조성하고 싶습니다. 2026년 10월 착공 희망합니다.</textarea>
<textarea id="clientNote" class="form-textarea" rows="3" placeholder="추구하시는 분위기, 선호 자재, 착공 희망 시기 등을 자유롭게 적어주세요."></textarea>
</div>
<button type="button" class="btn-materia" id="btnSubmitConsult" style="width: 100%; padding: 14px; margin-top: 10px;">
프라이빗 디자인 상담 신청 완료
입력한 상담 항목 확인
</button>
</form>
</div>
@ -378,9 +374,8 @@
<!-- 푸터 (.site-foot SSOT) -->
<footer class="site-foot">
<div class="wrap foot-inner">
<p>마테리아 스튜디오 · 서울 강남구 압구정로 72길 14</p>
<p class="muted">월요일부터 금요일까지 (사전 예약제)</p>
<p class="muted">studio@materia.kr</p>
<p>마테리아 스튜디오 · 공간 설계 안내</p>
<p class="muted">이 페이지의 프로젝트·상담 항목은 예시이며 입력 정보는 저장·전송되지 않습니다.</p>
</div>
</footer>
@ -412,7 +407,7 @@
const name = document.getElementById("clientName").value || "고객님";
const spaceType = document.getElementById("spaceType").value;
const area = document.getElementById("spaceArea").value;
alert(`[마테리아 스튜디오] ${name}님의 프라이빗 공간 컨설팅 신청(${spaceType} / ${area})이 접수되었습니다.\n담당 수석 디렉터가 24시간 이내에 포트폴리오 자료와 함께 직접 연락드리겠습니다.`);
alert(`${name}님이 입력한 상담 항목을 확인했습니다. (${spaceType} / ${area || "면적 미입력"})\n실제 상담은 접수되지 않았으며 입력 정보는 저장하거나 전송하지 않습니다.`);
});
}
})();

View file

@ -9,10 +9,7 @@
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/webfontworld/naver/MaruBuri.css"
/>
<link rel="stylesheet" href="../../styles/fonts.css?v=20260912all" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css"
@ -21,8 +18,9 @@
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap"
/>
<link rel="stylesheet" href="../../styles/tokens.css" />
<link rel="stylesheet" href="../../styles/main.css" />
<link rel="stylesheet" href="../../styles/tokens.css?v=20260912all" />
<link rel="stylesheet" href="../../styles/main.css?v=20260912all" />
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23F5F3EF'/%3E%3Crect x='6' y='6' width='20' height='20' stroke='%23161514' stroke-width='2' fill='none'/%3E%3Crect x='12' y='12' width='8' height='8' fill='%239E8062'/%3E%3C/svg%3E" />
<style>
/* Detail Page Specific Styles */
.breadcrumb-bar {
@ -188,7 +186,9 @@
background: var(--surface);
color: var(--ink-muted);
cursor: pointer;
transition: all 0.2s ease;
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
min-height: var(--control-block);
white-space: nowrap;
}
.gallery-chip:hover {
border-color: var(--accent);
@ -217,6 +217,7 @@
border: 1px solid var(--line);
border-radius: var(--radius-sm);
overflow: hidden;
padding: var(--space-2);
display: flex;
flex-direction: column;
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
@ -228,11 +229,29 @@
border-color: var(--accent);
box-shadow: 0 12px 28px rgba(0,0,0,0.06);
}
.gallery-card:focus-within {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-light);
}
.gallery-card-action {
position: absolute;
inset: 0;
z-index: 2;
border: 0;
border-radius: inherit;
background: transparent;
cursor: pointer;
}
.gallery-card-action:focus-visible {
outline: 3px solid var(--accent);
outline-offset: -3px;
}
.gallery-card-img-wrap {
aspect-ratio: 16 / 10;
position: relative;
overflow: hidden;
background: var(--surface-sunken);
border-radius: var(--radius-xs);
}
.gallery-card img {
width: 100%;
@ -244,7 +263,7 @@
transform: scale(1.03);
}
.gallery-card-body {
padding: var(--space-4);
padding: var(--space-3) var(--space-2) var(--space-2);
display: flex;
flex-direction: column;
gap: 6px;
@ -268,17 +287,26 @@
color: var(--ink-muted);
line-height: 1.5;
}
@media (max-width: 600px) {
.spatial-grid { grid-template-columns: minmax(0, 1fr); }
.gallery-chip { min-height: var(--control-block); padding-inline: var(--control-pad-inline); }
}
/* Lightbox Dialog */
.lightbox-dialog {
position: fixed;
inset: 0;
box-sizing: border-box;
margin: auto;
background: rgba(14, 14, 16, 0.96);
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: var(--radius-sm);
max-width: 1200px;
width: 95vw;
max-height: 92dvh;
width: min(1200px, calc(100dvw - (var(--space-3) * 2)));
max-height: min(92dvh, calc(100dvh - (var(--space-3) * 2)));
padding: 0;
overflow: auto;
overscroll-behavior: contain;
box-shadow: 0 32px 80px rgba(0, 0, 0, 0.8);
}
.lightbox-dialog::backdrop {
@ -288,6 +316,8 @@
.lightbox-layout {
display: grid;
grid-template-columns: 1fr;
min-height: 0;
max-height: inherit;
}
@media (min-width: 900px) {
.lightbox-layout {
@ -309,6 +339,7 @@
border-radius: var(--radius-xs);
}
.lightbox-info {
min-height: 0;
padding: var(--space-6);
display: flex;
flex-direction: column;
@ -349,6 +380,7 @@
</style>
</head>
<body data-ambiance="day">
<a class="skip-link" href="#main-content">본문으로 건너뛰기</a>
<!-- 상단 헤더 (.site-head SSOT) -->
<header class="site-head">
<div class="wrap head-inner">
@ -368,7 +400,7 @@
<button type="button" class="ambiance-btn" id="btnAmbianceNight">Night</button>
</div>
<a href="../../index.html#contact" class="btn">
상담 신청
상담 항목 보기
</a>
</div>
</div>
@ -392,6 +424,7 @@
<p class="hero-lead" style="font-size: var(--step-0); color: var(--ink-muted); max-width: var(--measure); margin-bottom: var(--space-5);">
5.8m 보이드 천장고와 로만 트래버틴, 3,000년 훈증 오크가 빚어낸 침묵의 공간.
</p>
<p class="demo-notice" role="note">안내: 아래 프로젝트 설명·이미지·수치·갤러리는 공간 연출 예시입니다. 실제 매물·시공·상담을 제공하지 않습니다.</p>
<dl class="project-specs" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: var(--space-4); background: var(--surface-card); border: 1px solid var(--line); border-radius: var(--radius); padding: var(--space-4) var(--space-5); margin-bottom: var(--space-6);">
<div>
@ -426,8 +459,8 @@
<img
src="../../assets/hannam-01-living-void.webp"
alt="한남 듀플렉스 펜트하우스 5.8m 보이드 거실 전경과 트래버틴 벽체"
width="1600"
height="900"
width="1376"
height="768"
style="width: 100%; aspect-ratio: 16/9; object-fit: cover;"
/>
<figcaption style="padding: 10px 16px; font-family: var(--font-mono); font-size: 12px; color: var(--ink-muted); border-top: 1px solid var(--line); background: var(--surface-raised);">
@ -475,20 +508,20 @@
<!-- Zone Filters -->
<div class="gallery-filter-chips" role="tablist" aria-label="공간 존별 필터">
<button type="button" class="gallery-chip active" data-zone="all">전체 공간 (24)</button>
<button type="button" class="gallery-chip" data-zone="living">Zone A: 거실 & 다이닝 (4)</button>
<button type="button" class="gallery-chip" data-zone="master">Zone B: 마스터 스위트 & 스파 (4)</button>
<button type="button" class="gallery-chip" data-zone="study">Zone C: 서재 & 와인 & 드레스룸 (4)</button>
<button type="button" class="gallery-chip" data-zone="garden">Zone D: 테라스 & 중정 & 현관 (4)</button>
<button type="button" class="gallery-chip" data-zone="materials">Zone E: 물성 매크로 접사 (4)</button>
<button type="button" class="gallery-chip" data-zone="analysis">Zone F: 도면 & 광학 분석 & 디테일 (4)</button>
<button type="button" class="gallery-chip" data-zone="living">Zone A · 거실·다이닝 (4)</button>
<button type="button" class="gallery-chip" data-zone="master">Zone B · 마스터·스파 (4)</button>
<button type="button" class="gallery-chip" data-zone="study">Zone C · 서재·와인·드레스룸 (4)</button>
<button type="button" class="gallery-chip" data-zone="garden">Zone D · 테라스·중정·현관 (4)</button>
<button type="button" class="gallery-chip" data-zone="materials">Zone E · 물성 접사 (4)</button>
<button type="button" class="gallery-chip" data-zone="analysis">Zone F · 도면·광학·디테일 (4)</button>
</div>
<!-- 24 Gallery Cards Grid -->
<div class="spatial-grid" id="spatialGrid">
<!-- Card 1 -->
<div class="gallery-card" data-zone="living" onclick="openLightbox(0)">
<div class="gallery-card" data-zone="living">
<div class="gallery-card-img-wrap">
<img src="../../assets/hannam-01-living-void.webp" alt="5.8m 보이드 거실" loading="lazy" />
<img src="../../assets/hannam-01-living-void.webp" width="1376" height="768" alt="5.8m 보이드 거실" loading="lazy" />
</div>
<div class="gallery-card-body">
<span class="gallery-card-zone">Zone A · Living</span>
@ -498,9 +531,9 @@
</div>
<!-- Card 2 -->
<div class="gallery-card" data-zone="living" onclick="openLightbox(1)">
<div class="gallery-card" data-zone="living">
<div class="gallery-card-img-wrap">
<img src="../../assets/hannam-03-dining.webp" alt="다이닝 룸" loading="lazy" />
<img src="../../assets/hannam-03-dining.webp" width="1376" height="768" alt="다이닝 룸" loading="lazy" />
</div>
<div class="gallery-card-body">
<span class="gallery-card-zone">Zone A · Dining</span>
@ -510,9 +543,9 @@
</div>
<!-- Card 3 -->
<div class="gallery-card" data-zone="living" onclick="openLightbox(2)">
<div class="gallery-card" data-zone="living">
<div class="gallery-card-img-wrap">
<img src="../../assets/hannam-04-kitchen.webp" alt="셰프 키친 아일랜드" loading="lazy" />
<img src="../../assets/hannam-04-kitchen.webp" width="1376" height="768" alt="셰프 키친 아일랜드" loading="lazy" />
</div>
<div class="gallery-card-body">
<span class="gallery-card-zone">Zone A · Kitchen</span>
@ -522,9 +555,9 @@
</div>
<!-- Card 4 -->
<div class="gallery-card" data-zone="living" onclick="openLightbox(3)">
<div class="gallery-card" data-zone="living">
<div class="gallery-card-img-wrap">
<img src="../../assets/hannam-10-staircase.webp" alt="부유형 캔틸레버 계단" loading="lazy" />
<img src="../../assets/hannam-10-staircase.webp" width="1376" height="768" alt="부유형 캔틸레버 계단" loading="lazy" />
</div>
<div class="gallery-card-body">
<span class="gallery-card-zone">Zone A · Staircase</span>
@ -534,9 +567,9 @@
</div>
<!-- Card 5 -->
<div class="gallery-card" data-zone="master" onclick="openLightbox(4)">
<div class="gallery-card" data-zone="master">
<div class="gallery-card-img-wrap">
<img src="../../assets/hannam-05-master-bedroom.webp" alt="마스터 침실" loading="lazy" />
<img src="../../assets/hannam-05-master-bedroom.webp" width="1376" height="768" alt="마스터 침실" loading="lazy" />
</div>
<div class="gallery-card-body">
<span class="gallery-card-zone">Zone B · Master Suite</span>
@ -546,9 +579,9 @@
</div>
<!-- Card 6 -->
<div class="gallery-card" data-zone="master" onclick="openLightbox(5)">
<div class="gallery-card" data-zone="master">
<div class="gallery-card-img-wrap">
<img src="../../assets/hannam-06-spa-bathroom.webp" alt="스파 욕실" loading="lazy" />
<img src="../../assets/hannam-06-spa-bathroom.webp" width="1376" height="768" alt="스파 욕실" loading="lazy" />
</div>
<div class="gallery-card-body">
<span class="gallery-card-zone">Zone B · Spa Bath</span>
@ -558,9 +591,9 @@
</div>
<!-- Card 7 -->
<div class="gallery-card" data-zone="master" onclick="openLightbox(6)">
<div class="gallery-card" data-zone="master">
<div class="gallery-card-img-wrap">
<img src="../../assets/hannam-13-powder-room.webp" alt="파우더룸" loading="lazy" />
<img src="../../assets/hannam-13-powder-room.webp" width="1376" height="768" alt="파우더룸" loading="lazy" />
</div>
<div class="gallery-card-body">
<span class="gallery-card-zone">Zone B · Powder Room</span>
@ -570,9 +603,9 @@
</div>
<!-- Card 8 -->
<div class="gallery-card" data-zone="master" onclick="openLightbox(7)">
<div class="gallery-card" data-zone="master">
<div class="gallery-card-img-wrap">
<img src="../../assets/hannam-14-guest-suite.webp" alt="게스트 침실" loading="lazy" />
<img src="../../assets/hannam-14-guest-suite.webp" width="1376" height="768" alt="게스트 침실" loading="lazy" />
</div>
<div class="gallery-card-body">
<span class="gallery-card-zone">Zone B · Guest Suite</span>
@ -582,9 +615,9 @@
</div>
<!-- Card 9 -->
<div class="gallery-card" data-zone="study" onclick="openLightbox(8)">
<div class="gallery-card" data-zone="study">
<div class="gallery-card-img-wrap">
<img src="../../assets/hannam-07-library-audio.webp" alt="서재 및 오디오 룸" loading="lazy" />
<img src="../../assets/hannam-07-library-audio.webp" width="1376" height="768" alt="서재 및 오디오 룸" loading="lazy" />
</div>
<div class="gallery-card-body">
<span class="gallery-card-zone">Zone C · Library</span>
@ -594,21 +627,21 @@
</div>
<!-- Card 10 -->
<div class="gallery-card" data-zone="study" onclick="openLightbox(9)">
<div class="gallery-card" data-zone="study">
<div class="gallery-card-img-wrap">
<img src="../../assets/hannam-11-dressing-room.webp" alt="드레스룸" loading="lazy" />
<img src="../../assets/hannam-11-dressing-room.webp" width="1376" height="768" alt="드레스룸" loading="lazy" />
</div>
<div class="gallery-card-body">
<span class="gallery-card-zone">Zone C · Dressing Room</span>
<h3 class="gallery-card-title">비스포크 워크인 드레스룸</h3>
<p class="gallery-card-desc">플루티드 브론즈 유리장과 시계/주얼리 수납 아일랜드 쇼케이스.</p>
<p class="gallery-card-desc">플루티드 브론즈 유리장과 시계·주얼리 수납 아일랜드.</p>
</div>
</div>
<!-- Card 11 -->
<div class="gallery-card" data-zone="study" onclick="openLightbox(10)">
<div class="gallery-card" data-zone="study">
<div class="gallery-card-img-wrap">
<img src="../../assets/hannam-12-wine-cellar.webp" alt="와인 셀러 룸" loading="lazy" />
<img src="../../assets/hannam-12-wine-cellar.webp" width="1376" height="768" alt="와인 셀러 룸" loading="lazy" />
</div>
<div class="gallery-card-body">
<span class="gallery-card-zone">Zone C · Wine Cellar</span>
@ -618,9 +651,9 @@
</div>
<!-- Card 12 -->
<div class="gallery-card" data-zone="study" onclick="openLightbox(11)">
<div class="gallery-card" data-zone="study">
<div class="gallery-card-img-wrap">
<img src="../../assets/hannam-20-tea-room.webp" alt="티 룸 알코브" loading="lazy" />
<img src="../../assets/hannam-20-tea-room.webp" width="1376" height="768" alt="티 룸 알코브" loading="lazy" />
</div>
<div class="gallery-card-body">
<span class="gallery-card-zone">Zone C · Tea Alcove</span>
@ -630,9 +663,9 @@
</div>
<!-- Card 13 -->
<div class="gallery-card" data-zone="garden" onclick="openLightbox(12)">
<div class="gallery-card" data-zone="garden">
<div class="gallery-card-img-wrap">
<img src="../../assets/hannam-08-terrace.webp" alt="프라이빗 테라스" loading="lazy" />
<img src="../../assets/hannam-08-terrace.webp" width="1376" height="768" alt="프라이빗 테라스" loading="lazy" />
</div>
<div class="gallery-card-body">
<span class="gallery-card-zone">Zone D · Terrace</span>
@ -642,9 +675,9 @@
</div>
<!-- Card 14 -->
<div class="gallery-card" data-zone="garden" onclick="openLightbox(13)">
<div class="gallery-card" data-zone="garden">
<div class="gallery-card-img-wrap">
<img src="../../assets/hannam-09-foyer.webp" alt="입구 현관 전경" loading="lazy" />
<img src="../../assets/hannam-09-foyer.webp" width="1376" height="768" alt="입구 현관 전경" loading="lazy" />
</div>
<div class="gallery-card-body">
<span class="gallery-card-zone">Zone D · Entrance</span>
@ -654,9 +687,9 @@
</div>
<!-- Card 15 -->
<div class="gallery-card" data-zone="garden" onclick="openLightbox(14)">
<div class="gallery-card" data-zone="garden">
<div class="gallery-card-img-wrap">
<img src="../../assets/hannam-22-lightwell.webp" alt="실내 석경 중정" loading="lazy" />
<img src="../../assets/hannam-22-lightwell.webp" width="1376" height="768" alt="실내 석경 중정" loading="lazy" />
</div>
<div class="gallery-card-body">
<span class="gallery-card-zone">Zone D · Lightwell</span>
@ -666,9 +699,9 @@
</div>
<!-- Card 16 -->
<div class="gallery-card" data-zone="garden" onclick="openLightbox(15)">
<div class="gallery-card" data-zone="garden">
<div class="gallery-card-img-wrap">
<img src="../../assets/hannam-21-art-niche.webp" alt="갤러리 아트 니치" loading="lazy" />
<img src="../../assets/hannam-21-art-niche.webp" width="1376" height="768" alt="갤러리 아트 니치" loading="lazy" />
</div>
<div class="gallery-card-body">
<span class="gallery-card-zone">Zone D · Art Niche</span>
@ -678,9 +711,9 @@
</div>
<!-- Card 17 -->
<div class="gallery-card" data-zone="materials" onclick="openLightbox(16)">
<div class="gallery-card" data-zone="materials">
<div class="gallery-card-img-wrap">
<img src="../../assets/hannam-15-mat-travertine.webp" alt="로만 트래버틴 매크로" loading="lazy" />
<img src="../../assets/hannam-15-mat-travertine.webp" width="1376" height="768" alt="로만 트래버틴 매크로" loading="lazy" />
</div>
<div class="gallery-card-body">
<span class="gallery-card-zone">Zone E · Material</span>
@ -690,9 +723,9 @@
</div>
<!-- Card 18 -->
<div class="gallery-card" data-zone="materials" onclick="openLightbox(17)">
<div class="gallery-card" data-zone="materials">
<div class="gallery-card-img-wrap">
<img src="../../assets/hannam-16-mat-bogoak.webp" alt="3000년 훈증 오크 매크로" loading="lazy" />
<img src="../../assets/hannam-16-mat-bogoak.webp" width="1376" height="768" alt="3000년 훈증 오크 매크로" loading="lazy" />
</div>
<div class="gallery-card-body">
<span class="gallery-card-zone">Zone E · Material</span>
@ -702,9 +735,9 @@
</div>
<!-- Card 19 -->
<div class="gallery-card" data-zone="materials" onclick="openLightbox(18)">
<div class="gallery-card" data-zone="materials">
<div class="gallery-card-img-wrap">
<img src="../../assets/hannam-17-mat-linen.webp" alt="벨기에 리넨 매크로" loading="lazy" />
<img src="../../assets/hannam-17-mat-linen.webp" width="1376" height="768" alt="벨기에 리넨 매크로" loading="lazy" />
</div>
<div class="gallery-card-body">
<span class="gallery-card-zone">Zone E · Material</span>
@ -714,9 +747,9 @@
</div>
<!-- Card 20 -->
<div class="gallery-card" data-zone="materials" onclick="openLightbox(19)">
<div class="gallery-card" data-zone="materials">
<div class="gallery-card-img-wrap">
<img src="../../assets/hannam-18-mat-bronze.webp" alt="파티나 브론즈 매크로" loading="lazy" />
<img src="../../assets/hannam-18-mat-bronze.webp" width="1376" height="768" alt="파티나 브론즈 매크로" loading="lazy" />
</div>
<div class="gallery-card-body">
<span class="gallery-card-zone">Zone E · Material</span>
@ -726,9 +759,9 @@
</div>
<!-- Card 21 -->
<div class="gallery-card" data-zone="analysis" onclick="openLightbox(20)">
<div class="gallery-card" data-zone="analysis">
<div class="gallery-card-img-wrap">
<img src="../../assets/hannam-02-blueprint.webp" alt="건축 평면 설계도면" loading="lazy" />
<img src="../../assets/hannam-02-blueprint.webp" width="1376" height="768" alt="건축 평면 설계도면" loading="lazy" />
</div>
<div class="gallery-card-body">
<span class="gallery-card-zone">Zone F · Blueprint</span>
@ -738,9 +771,9 @@
</div>
<!-- Card 22 -->
<div class="gallery-card" data-zone="analysis" onclick="openLightbox(21)">
<div class="gallery-card" data-zone="analysis">
<div class="gallery-card-img-wrap">
<img src="../../assets/hannam-23-light-simulation.webp" alt="3D 일조 광학 시뮬레이션" loading="lazy" />
<img src="../../assets/hannam-23-light-simulation.webp" width="1376" height="768" alt="3D 일조 광학 시뮬레이션" loading="lazy" />
</div>
<div class="gallery-card-body">
<span class="gallery-card-zone">Zone F · Analysis</span>
@ -750,9 +783,9 @@
</div>
<!-- Card 23 -->
<div class="gallery-card" data-zone="analysis" onclick="openLightbox(22)">
<div class="gallery-card" data-zone="analysis">
<div class="gallery-card-img-wrap">
<img src="../../assets/hannam-19-joinery.webp" alt="5mm 섀도우 갭 조인트" loading="lazy" />
<img src="../../assets/hannam-19-joinery.webp" width="1376" height="768" alt="5mm 섀도우 갭 조인트" loading="lazy" />
</div>
<div class="gallery-card-body">
<span class="gallery-card-zone">Zone F · Detail</span>
@ -762,9 +795,9 @@
</div>
<!-- Card 24 -->
<div class="gallery-card" data-zone="analysis" onclick="openLightbox(23)">
<div class="gallery-card" data-zone="analysis">
<div class="gallery-card-img-wrap">
<img src="../../assets/hannam-24-timber-joinery.webp" alt="장인 장부 맞춤 목공" loading="lazy" />
<img src="../../assets/hannam-24-timber-joinery.webp" width="1376" height="768" alt="장인 장부 맞춤 목공" loading="lazy" />
</div>
<div class="gallery-card-body">
<span class="gallery-card-zone">Zone F · Detail</span>
@ -781,7 +814,7 @@
<div class="wrap">
<div class="blueprint-viewer">
<div class="blueprint-frame">
<img src="../../assets/hannam-02-blueprint.webp" alt="한남 듀플렉스 펜트하우스 평면도" />
<img src="../../assets/hannam-02-blueprint.webp" width="1376" height="768" alt="한남 듀플렉스 펜트하우스 평면도" />
</div>
<div style="display: flex; flex-direction: column; gap: var(--space-4);">
<div class="eyebrow" style="color: var(--accent);">물성 사양</div>
@ -800,7 +833,7 @@
</div>
</div>
<a href="../../index.html#consultation" class="btn-primary" style="display: inline-block; text-align: center; text-decoration: none; margin-top: var(--space-2);">
이 프로젝트 스타일로 상담 신청하기 →
이 프로젝트의 상담 항목 보기 →
</a>
</div>
</div>
@ -812,7 +845,7 @@
<dialog id="lightboxDialog" class="lightbox-dialog">
<div class="lightbox-layout">
<div class="lightbox-media">
<img id="lightboxImg" src="../../assets/hannam-01-living-void.webp" alt="확대 이미지" />
<img id="lightboxImg" src="../../assets/hannam-01-living-void.webp" width="1376" height="768" alt="확대 이미지" />
</div>
<div class="lightbox-info">
<div>
@ -837,9 +870,8 @@
<!-- 푸터 (.site-foot SSOT) -->
<footer class="site-foot">
<div class="wrap foot-inner">
<p>마테리아 스튜디오 · 서울 강남구 압구정로 72길 14</p>
<p class="muted">월요일부터 금요일까지 (사전 예약제)</p>
<p class="muted">studio@materia.kr</p>
<p>마테리아 스튜디오 · 공간 설계 안내</p>
<p class="muted">이 페이지의 프로젝트·상담 항목은 예시이며 입력 정보는 저장·전송되지 않습니다.</p>
</div>
</footer>
@ -849,14 +881,14 @@
const btnDay = document.getElementById('btnAmbianceDay');
const btnNight = document.getElementById('btnAmbianceNight');
btnDay?.addEventListener('click', () => {
document.documentElement.removeAttribute('data-theme');
btnDay.classList.add('active');
btnNight.classList.remove('active');
document.body.setAttribute('data-ambiance', 'day');
btnDay.classList.add('is-active');
btnNight.classList.remove('is-active');
});
btnNight?.addEventListener('click', () => {
document.documentElement.setAttribute('data-theme', 'night');
btnNight.classList.add('active');
btnDay.classList.remove('active');
document.body.setAttribute('data-ambiance', 'night');
btnNight.classList.add('is-active');
btnDay.classList.remove('is-active');
});
// 2. 24 Gallery Items Data Store
@ -870,7 +902,7 @@
{ 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-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-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: '대형 트래버틴 페이버와 조형 소나무 분재, 한강 석양 조망.' },
@ -891,6 +923,19 @@
const filterChips = document.querySelectorAll('.gallery-chip');
const galleryCards = document.querySelectorAll('.gallery-card');
// 전체 카드를 덮는 실제 button으로 마우스·터치·키보드가 같은 라이트박스를 연다.
galleryCards.forEach((card, index) => {
card.removeAttribute('onclick');
const action = document.createElement('button');
action.type = 'button';
action.className = 'gallery-card-action';
action.setAttribute('aria-haspopup', 'dialog');
action.setAttribute('aria-controls', 'lightboxDialog');
action.setAttribute('aria-label', `${card.querySelector('.gallery-card-title').textContent.trim()} 상세 보기`);
action.addEventListener('click', () => openLightbox(index));
card.append(action);
});
filterChips.forEach(chip => {
chip.addEventListener('click', () => {
filterChips.forEach(c => c.classList.remove('active'));

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