diff --git a/apps/site/design.md b/apps/site/design.md index 9cf8ee6..659359f 100644 --- a/apps/site/design.md +++ b/apps/site/design.md @@ -1103,3 +1103,33 @@ Pretendard `dynamic-subset` 은 유니코드 범위별로 파일을 쪼개고 - `npm run verify`: 위 트웬티온스 전용 게이트와 저장소 전체 L0~L6를 한 번에 실행한다. - 3D 번들 예산은 gzip 200KB 이하이며 현재 기준은 166,225B다. GLB는 697,076B이며 로컬 폰트·폴백 이미지를 포함한 예산은 전용 E2E에서 하드 실패한다. - 2026-08-25 국소 조정: 3D 화면 중심은 모바일 50%, 태블릿 74%, 소형 PC 64%, 와이드 PC 60%를 기준으로 하며, 중복 현장 상태는 보조기술에만 알리고 3D 모션 제어는 히어로 CTA 행에 둔다. + +## RFS CFD · 새벽청과 연장 업그레이드 (2026-08-25) + +### 유지한 방향 + +- 신규 콘셉트로 갈아엎지 않고 기존의 검증된 대비를 연장한다. RFS는 **암실과 사양서**, 새벽청과는 **오늘 새벽 두 시의 값**을 계속 기준으로 삼는다. +- RFS의 실제 시뮬레이션 렌더와 새벽청과의 실물 과일 사진을 유지했다. 생성 이미지로 제품 증거나 산지 품질을 꾸미면 실제 출력·실물처럼 오해될 수 있으므로 이 판에서는 이미지 생성 도구를 쓰지 않았다. +- 장식 면적을 늘리지 않고 사용자가 정보를 읽은 다음 내릴 수 있는 판단과 행동을 추가했다. + +### RFS — 조작에서 해석으로 + +- 히어로에 `기준 운전 · 랙 과부하 · 급기 저하` 프리셋을 두고 서버랙 부하와 FWU 급기를 실제 슬라이더 값으로 함께 바꾼다. +- 부하와 급기의 차이를 읽어 `현재 상태 · 먼저 볼 곳 · 다음 조작`을 한국어로 해석한다. 정확도나 온도 수치는 새로 만들지 않는다. +- 상면·측면 실물 렌더를 한 뷰어의 전환 상태로 합쳐 두 이미지가 세로로 쌓이며 생기던 공백을 없앴다. +- 감소 모션이나 WebGL 실패에서도 프리셋·해석·렌더 전환은 작동하고, 유동 화면만 정적으로 남는다. + +### 새벽청과 — 시세에서 발주 메모로 + +- 경매판의 여섯 품목을 선택 가능한 로트로 바꾸고 산지·등급·당도·규격·낙찰가·전일 대비를 하나의 판독기에 동기화한다. +- `오늘 경매`는 하드코딩 날짜가 아니라 한국 표준시의 실제 날짜로 표시한다. +- 제철 띠에 이전 달·다음 달 탐색과 해당 달의 품목 목록을 붙였다. +- 선택한 로트를 발주 메모로 이어 최소 3상자·최대 50상자 범위와 예시 낙찰가 합계를 계산한다. 선별·포장·배송·실제 납품가는 포함되지 않는다고 바로 아래에 적었다. +- 확인되지 않은 가상 이메일 주소는 제거했다. 복사는 클립보드 API가 응답하지 않으면 1.2초 뒤 선택 복사로 전환하며, 성공·실패를 상태 문장으로 알린다. + +### 검증 기준과 현재 증거 + +- `npm run test:e2e:recent-showcases`는 320/390/768/1440 반응형, 터치 타깃, 왼쪽 인셋, 이미지, RFS 프리셋·수동 조정·렌더 전환·감소 모션, 새벽청과 로트·제철·수량 경계·계산·실제 클립보드, 두 페이지 axe·JS 없음·런타임 오류를 검사한다. +- 최초 실행에서 CSS 토큰 경로 404와 응답하지 않는 클립보드 권한을 각각 재현했다. 토큰을 명시적 스타일 링크로 바꾸고 복사 폴백을 추가한 뒤 성능 계측을 포함해 **115/115 통과**했다. +- `showcase-audit.mjs rfs-cfd saebyeok-cheonggwa`는 두 페이지 × 320/390/768/1440, 총 **8/8 통과**했다. +- 위 전용 E2E는 저장소 전체 `verify.mjs`의 L6에 포함한다. 배포 전에는 전체 게이트를 다시 실행한다. diff --git a/apps/site/gate-report.md b/apps/site/gate-report.md index 7aaeac5..d2fd826 100644 --- a/apps/site/gate-report.md +++ b/apps/site/gate-report.md @@ -1,4 +1,4 @@ -# 게이트 리포트 — 2026-08-24 17:12 +# 게이트 리포트 — 2026-08-24 19:37 **전 항목 통과 — 배포 가능** diff --git a/apps/site/package.json b/apps/site/package.json index 3551f7f..022cc21 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -8,6 +8,7 @@ "build:showcase-assets": "node tools/build-showcase-assets.mjs", "test:design:gate": "node tools/design-gate.mjs --config=gate.config.json", "test:design:products": "node tools/design-gate.mjs --config=gate.twentyoz-products.config.json", + "test:e2e:recent-showcases": "node tools/recent-showcases-e2e.mjs", "test:e2e:twentyoz": "node tools/twentyoz-e2e.mjs", "prebuild": "npm run build:showcase-assets", "build": "astro build", diff --git a/apps/site/public/work/rfs-cfd/index.html b/apps/site/public/work/rfs-cfd/index.html index ad9bd1b..339b065 100644 --- a/apps/site/public/work/rfs-cfd/index.html +++ b/apps/site/public/work/rfs-cfd/index.html @@ -27,7 +27,8 @@ rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&display=swap" /> - + + +
운전 장면
+ + + +브라우저 데모 해석
+냉기와 발열이 맞서는 기준 상태입니다.
+위 화면은 브라우저에서 도는 2D 축소판이다. 슬라이더를 움직이거나 화면을 문지르면 유동이 즉시 반응한다. 실물 RFS 는 Unity 6.4 위에서 3D δ-SPH 를 돌린다 — @@ -329,28 +385,42 @@
-
-
+
+ 두 장 모두 RFS 실물의 파티클 뷰다. 저온이 청록, 고온이 초록이다 — 이 페이지 첫 화면의 색도 같은 규칙을 쓴다. diff --git a/apps/site/public/work/rfs-cfd/scripts/thermal-field.js b/apps/site/public/work/rfs-cfd/scripts/thermal-field.js index 6138126..9a11b1d 100644 --- a/apps/site/public/work/rfs-cfd/scripts/thermal-field.js +++ b/apps/site/public/work/rfs-cfd/scripts/thermal-field.js @@ -34,6 +34,101 @@ const shell = canvas.closest("[data-field-shell]"); const setState = (s) => shell && shell.setAttribute("data-field-state", s); + /* ── 데모 조작 UI ──────────────────────────────────────────────── + WebGL 이 없거나 모션을 줄여도 조작의 의미는 남아야 한다. + 그래서 슬라이더·장면·실물 뷰 전환은 그래픽 초기화보다 먼저 묶는다. */ + const controls = { + load: Number(document.getElementById("ctlLoad")?.value ?? 70) / 100, + supply: Number(document.getElementById("ctlSupply")?.value ?? 65) / 100, + }; + + const presetButtons = [...document.querySelectorAll("[data-field-preset]")]; + const fieldState = document.getElementById("fieldState"); + const fieldScene = document.getElementById("fieldScene"); + const fieldWatch = document.getElementById("fieldWatch"); + const fieldAction = document.getElementById("fieldAction"); + + const activePreset = () => presetButtons.find((button) => button.getAttribute("aria-pressed") === "true"); + + function updateReadout() { + const gap = Math.round((controls.load - controls.supply) * 100); + const selected = activePreset(); + if (fieldScene) fieldScene.textContent = selected?.textContent.trim() || "수동 조정"; + + let state = "냉기와 발열이 맞서는 기준 상태입니다."; + let watch = "랙 배면"; + let action = "부하와 급기를 따로 움직여 비교"; + + if (gap >= 35) { + state = "발열이 급기보다 크게 앞섭니다. 배면에서 천장으로 오르는 열기둥을 확인하십시오."; + watch = "랙 배면 · 천장 배기"; + action = "급기 상향과 부하 재배분을 비교"; + } else if (gap >= 15) { + state = "발열 우세 구간입니다. 랙 뒤에 머무는 고온 입자의 경로를 비교하십시오."; + watch = "랙 배면"; + action = "급기량을 한 단계씩 높여 비교"; + } else if (gap <= -15) { + state = "급기가 발열을 앞섭니다. FWU 제트가 만드는 저온 구간을 확인하십시오."; + watch = "FWU 제트 · 냉점"; + action = "과냉 구간과 팬 운전을 함께 검토"; + } + + if (fieldState) fieldState.textContent = state; + if (fieldWatch) fieldWatch.textContent = watch; + if (fieldAction) fieldAction.textContent = action; + } + + function matchPreset() { + const load = Math.round(controls.load * 100); + const supply = Math.round(controls.supply * 100); + let matched = false; + for (const button of presetButtons) { + const same = Number(button.dataset.load) === load && Number(button.dataset.supply) === supply; + button.setAttribute("aria-pressed", String(same)); + matched ||= same; + } + if (!matched) presetButtons.forEach((button) => button.setAttribute("aria-pressed", "false")); + } + + function bindSlider(id, key, outId, fmt) { + const el = document.getElementById(id); + const out = document.getElementById(outId); + if (!el) return; + const sync = () => { + controls[key] = Number(el.value) / 100; + if (out) out.textContent = fmt(Number(el.value)); + matchPreset(); + updateReadout(); + }; + el.addEventListener("input", sync); + sync(); + } + + bindSlider("ctlLoad", "load", "outLoad", (v) => `${v}%`); + bindSlider("ctlSupply", "supply", "outSupply", (v) => `${v}%`); + + for (const button of presetButtons) { + button.addEventListener("click", () => { + const load = document.getElementById("ctlLoad"); + const supply = document.getElementById("ctlSupply"); + if (!(load instanceof HTMLInputElement) || !(supply instanceof HTMLInputElement)) return; + load.value = button.dataset.load || load.value; + supply.value = button.dataset.supply || supply.value; + load.dispatchEvent(new Event("input", { bubbles: true })); + supply.dispatchEvent(new Event("input", { bubbles: true })); + }); + } + + const visTabs = [...document.querySelectorAll("[data-vis-target]")]; + const visPanels = [...document.querySelectorAll("[data-vis-panel]")]; + for (const tab of visTabs) { + tab.addEventListener("click", () => { + const target = tab.dataset.visTarget; + for (const button of visTabs) button.setAttribute("aria-pressed", String(button === tab)); + for (const panel of visPanels) panel.hidden = panel.dataset.visPanel !== target; + }); + } + const reduced = window.matchMedia("(prefers-reduced-motion: reduce)"); if (reduced.matches) { setState("static"); @@ -450,27 +545,6 @@ } } - /* ── 조작 입력 ────────────────────────────────────────────────── */ - const controls = { - load: Number(document.getElementById("ctlLoad")?.value ?? 70) / 100, - supply: Number(document.getElementById("ctlSupply")?.value ?? 65) / 100, - }; - - function bindSlider(id, key, outId, fmt) { - const el = document.getElementById(id); - const out = document.getElementById(outId); - if (!el) return; - const sync = () => { - controls[key] = Number(el.value) / 100; - if (out) out.textContent = fmt(Number(el.value)); - }; - el.addEventListener("input", sync); - sync(); - } - - bindSlider("ctlLoad", "load", "outLoad", (v) => `${v}%`); - bindSlider("ctlSupply", "supply", "outSupply", (v) => `${v}%`); - const pointer = { x: 0, y: 0, on: 0 }; const onMove = (e) => { const r = canvas.getBoundingClientRect(); diff --git a/apps/site/public/work/rfs-cfd/styles/main.css b/apps/site/public/work/rfs-cfd/styles/main.css index a3431ce..c3f4eeb 100644 --- a/apps/site/public/work/rfs-cfd/styles/main.css +++ b/apps/site/public/work/rfs-cfd/styles/main.css @@ -1,5 +1,3 @@ -@import "./tokens.css"; - /* ========================================================================== RFS — 메인 스타일시트 @@ -361,8 +359,48 @@ h3 { color: var(--band-ink-muted); } -.hero-controls { +.scenario-strip { margin-top: var(--space-5); + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--space-2); + max-width: 760px; +} + +.scenario-label { + margin-right: var(--space-2); + font-family: var(--font-mono); + font-size: var(--step--1); + color: var(--band-ink-faint); +} + +.scenario-btn, +.vis-tab { + min-height: 40px; + padding-inline: var(--space-3); + border: 1px solid var(--band-line-strong); + background: transparent; + color: var(--band-ink-muted); + cursor: pointer; + font-size: var(--step--1); +} + +.scenario-btn:hover, +.vis-tab:hover { + color: var(--band-ink); + border-color: var(--band-accent); +} + +.scenario-btn[aria-pressed="true"], +.vis-tab[aria-pressed="true"] { + color: var(--band-accent-ink); + background: var(--band-accent); + border-color: var(--band-accent); +} + +.hero-controls { + margin-top: var(--space-4); display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--space-4); @@ -389,6 +427,58 @@ input[type="range"] { background: transparent; } +.field-readout { + margin-top: var(--space-5); + max-width: 880px; + border-block: 1px solid var(--band-line-strong); +} + +.field-readout-head { + display: grid; + gap: var(--space-2); + padding-block: var(--space-3); + border-bottom: 1px solid var(--band-line); +} + +.field-readout-label, +.field-readout-grid dt { + font-size: var(--step--1); + color: var(--band-ink-faint); +} + +.field-readout-state { + color: var(--band-ink); +} + +.field-readout-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.field-readout-grid > div { + min-width: 0; + padding-block: var(--space-3); +} + +.field-readout-grid > div + div { + padding-left: var(--space-3); + border-left: 1px solid var(--band-line); +} + +.field-readout-grid dd { + margin-top: var(--space-1); + color: var(--band-ink-muted); + font-size: var(--step--1); + line-height: var(--leading-normal); +} + +@media (min-width: 720px) { + .field-readout-head { + grid-template-columns: 9rem minmax(0, 1fr); + align-items: baseline; + } +} + .hero-note { margin-top: var(--space-5); padding-top: var(--space-3); @@ -692,17 +782,54 @@ input[type="range"] { .vis-shots { display: grid; - gap: var(--space-4); + gap: var(--space-3); + min-width: 0; +} + +.vis-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-3); + border-bottom: 1px solid var(--band-line-strong); + padding-bottom: var(--space-2); +} + +.vis-tabs { + display: flex; +} + +.vis-tab + .vis-tab { + border-left: 0; +} + +.vis-source { + color: var(--band-ink-faint); + font-size: var(--step--1); + text-align: right; +} + +.vis-frame { + min-width: 0; } .vis-shots figure { + display: grid; + grid-template-rows: minmax(0, 1fr) auto; border: 1px solid var(--band-line); background: var(--band-surface-2); + min-height: clamp(360px, 42vw, 540px); +} + +.vis-shots figure[hidden] { + display: none; } .vis-shots img { width: 100%; - height: auto; + height: 100%; + min-height: 0; + object-fit: contain; } .vis-shots figcaption { @@ -713,6 +840,35 @@ input[type="range"] { font-family: var(--font-mono); } +.temp-key { + display: flex; + flex-wrap: wrap; + gap: var(--space-3); + color: var(--band-ink-muted); + font-family: var(--font-mono); + font-size: var(--step--1); +} + +.temp-key span { + display: inline-flex; + align-items: center; + gap: var(--space-2); +} + +.temp-key i { + width: 1.5rem; + height: 2px; + background: var(--temp-cold); +} + +.temp-key .temp-mid { + background: var(--temp-mid); +} + +.temp-key .temp-hot { + background: var(--temp-hot); +} + .shots-note { font-size: var(--step--1); color: var(--band-ink-faint); @@ -979,6 +1135,35 @@ kbd { font-size: 0.6875rem; } + .scenario-strip { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .scenario-label { + grid-column: 1 / -1; + } + + .scenario-btn, + .vis-tab { + min-height: 44px; + padding-inline: var(--space-2); + } + + .field-readout-grid { + grid-template-columns: 1fr; + } + + .field-readout-grid > div + div { + padding-left: 0; + border-left: 0; + border-top: 1px solid var(--band-line); + } + + .vis-shots figure { + min-height: 320px; + } + .figures { grid-template-columns: repeat(2, 1fr); } @@ -1014,6 +1199,8 @@ kbd { .site-head, .hero-stage, .hero-controls, + .scenario-strip, + .field-readout, .skip-link { display: none; } diff --git a/apps/site/public/work/saebyeok-cheonggwa/index.html b/apps/site/public/work/saebyeok-cheonggwa/index.html index dbd6270..b747d46 100644 --- a/apps/site/public/work/saebyeok-cheonggwa/index.html +++ b/apps/site/public/work/saebyeok-cheonggwa/index.html @@ -24,7 +24,8 @@ rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&display=swap" /> - + +
선택 로트 읽기
+02:00 경매
+백도 복숭아
+경북 청도
++ + + 제철 품목을 읽는 중입니다 +
+