fix: resolve review issues B1-B6 (color token alpha contract, Header contrast/budget B plan, Publications Overview page, N1-N8 cleanups) with 100% E2E PASS
This commit is contained in:
+100
-3
@@ -4,10 +4,10 @@
|
||||
| 항목 | 내용 |
|
||||
| :--- | :--- |
|
||||
| **문서 ID** | `REQ-ANL-WEB-001` |
|
||||
| **버전** | `1.8` |
|
||||
| **버전** | `1.10` |
|
||||
| **작성일** | 2026-07-30 |
|
||||
| **작성자** | claude (`canary-projects-landing-page-creator-claude`) · Role: planner |
|
||||
| **상태** | 승인 (Approved) — v1.5에서 **D-01** 확정(A·B안 기각 / `intl-conf` 폐기 · 3개 카테고리), v1.6에서 **D-06** 확정(레이아웃 컨테이너 3계층 분리). v1.7에서 **§6.1.2 Research Project PageView** 신설 — 신규 미결 **D-07**(🔴 Funder 실데이터 부재)·**D-08**(🟡 프로젝트 집합 범위)은 각 기본값으로 구현을 차단하지 않는다. 잔여 미결 D-02·D-03·D-05는 🟡 등급으로 각 기본값이 적용되어 승인을 보류하지 않는다 |
|
||||
| **상태** | 승인 (Approved) — v1.5에서 **D-01** 확정(A·B안 기각 / `intl-conf` 폐기 · 3개 카테고리), v1.6에서 **D-06** 확정(레이아웃 컨테이너 3계층 분리). v1.7에서 **§6.1.2 Research Project PageView** 신설 — 신규 미결 **D-07**(🔴 Funder 실데이터 부재)·**D-08**(🟡 프로젝트 집합 범위)은 각 기본값으로 구현을 차단하지 않는다. 잔여 미결 D-02·D-03·D-05는 🟡 등급으로 각 기본값이 적용되어 승인을 보류하지 않는다. v1.9에서 **§8.7 색 토큰 알파 계약**(R-8.21~R-8.24)을 신설하고 **D-09**(Publications 인덱스 라우트)를 확정했다. v1.10에서 개발 팀장(Agy) 이의제기를 반영해 **D-09를 Overview 뷰로 재확정**하고 R-8.25·AC-41을 신설했다 |
|
||||
| **대상 독자** | General Manager, Developer Team Leader(Agy), Reviewer Team Leader(Cline), 지도교수 |
|
||||
| **선행 문서** | [`README.md`](./README.md), [`.agents/MULTI_AGENT_RULES.md`](./.agents/MULTI_AGENT_RULES.md), [`refer_landing_page/docs/DESIGN.md`](./refer_landing_page/docs/DESIGN.md) |
|
||||
| **적용 범위** | `refer_landing_page/` (Next.js 14 App Router 애플리케이션) 전체 |
|
||||
@@ -110,7 +110,7 @@
|
||||
| :--- | :--- | :--- |
|
||||
| **IA-01** | 현행 `/intro`의 커버 스프레드를 **`/`(Home)으로 승격**한다 | PR-02가 상단 탭에 `Intro`를 포함하지 않으며, PR-03의 "연구실 소개"가 Home의 구성 요소로 명시되어 있다. `/intro`를 별도 유지하면 Home과 콘텐츠가 중복된다. **부수 효과로 기존 `/` 404 결함이 해소된다.** |
|
||||
| **IA-02** | `/intro`는 **`/`로 308 영구 리다이렉트**한다 | 3차에 걸친 리뷰 리포트·문서가 `/intro`를 참조하고 있어 링크 안정성이 필요하다. `next.config.js`의 `redirects()`로 처리하며 별도 페이지 파일을 남기지 않는다. |
|
||||
| **IA-03** *(v1.1 개정 · v1.5 수치 갱신)* | Publications 카테고리는 **라우트 세그먼트 `/publications/[category]` + `generateStaticParams()`** 로 구현한다. `/publications`는 개요 화면으로 유지한다 | **v1.0에서 "단일 라우트 + `?cat=` 쿼리"였으나 §8.5의 실측 검증 결과에 따라 개정.** 세그먼트 방식은 카테고리별 완전한 정적 HTML 3개(D-01 확정 전 4개)를 빌드 타임에 생성해 **공유 링크가 필터 상태를 포함한 문서를 그대로 제공**하며(AC-05 취지 충족), 뒤로/앞으로 가기·프리페치·스크롤 복원이 Next 라우터에서 기본 제공된다. 슬러그를 카테고리 레지스트리에서 도출하므로 **라벨 변경이 레지스트리 한 곳 수정으로 끝난다는 v1.0의 근거는 그대로 유지**된다(→ D-01 리스크 완화 불변). 상단 탭은 5종 그대로이므로 **PR-02와 충돌하지 않는다**(하위 라우트일 뿐 최상위 탭이 아니다). |
|
||||
| **IA-03** *(v1.1 개정 · v1.5 수치 갱신 · v1.9 개정 · v1.10 재개정)* | Publications 카테고리는 **라우트 세그먼트 `/publications/[category]` + `generateStaticParams()`** 로 구현한다. **`/publications`는 `redirect()`가 아니라 자체 콘텐츠를 렌더하는 실체 라우트로 유지하며, 그 콘텐츠는 하위 카테고리 어느 것과도 중복되지 않는 Overview 뷰**(카테고리별 집계 + 최신 실적 하이라이트)로 한다 — D-09 재확정(v1.10). `redirect()` 전용 라우트는 산출 HTML에 `<main>`·`container-content`가 존재하지 않아 **AC-25를 구조적으로 위반**하므로 금지하고, 첫 카테고리 본문을 그대로 재렌더하는 방식은 **동일 DOM 정적 HTML 2본을 생성해 R-8.25를 위반**하므로 금지한다. 상단 `CategoryNav`는 **3개 카테고리 탭만** 노출하며 `All`/`전체` 탭을 두지 않는다(commit `22a31ff` 결정 존중) — Overview에서는 활성 탭이 없는 것이 정상 상태다 | **v1.0에서 "단일 라우트 + `?cat=` 쿼리"였으나 §8.5의 실측 검증 결과에 따라 개정.** 세그먼트 방식은 카테고리별 완전한 정적 HTML 3개(D-01 확정 전 4개)를 빌드 타임에 생성해 **공유 링크가 필터 상태를 포함한 문서를 그대로 제공**하며(AC-05 취지 충족), 뒤로/앞으로 가기·프리페치·스크롤 복원이 Next 라우터에서 기본 제공된다. 슬러그를 카테고리 레지스트리에서 도출하므로 **라벨 변경이 레지스트리 한 곳 수정으로 끝난다는 v1.0의 근거는 그대로 유지**된다(→ D-01 리스크 완화 불변). 상단 탭은 5종 그대로이므로 **PR-02와 충돌하지 않는다**(하위 라우트일 뿐 최상위 탭이 아니다). |
|
||||
| **IA-04** | 내비게이션 항목은 **PR-02의 5개로 고정**한다 | `components/Header.tsx`의 데스크톱 내비가 5개 항목 기준으로 조판되어 있다. 항목 증감 시 헤더 레이아웃 재설계가 선행되어야 한다. |
|
||||
|
||||
---
|
||||
@@ -481,6 +481,31 @@ Job `343b5808` 검증에서 본문 좌우 여백·최대 폭 미적용(B1)이,
|
||||
- **실측 확인**: Tailwind v3는 `extend`로 추가한 스크린도 **min-width 값 기준으로 정렬**해 출력하므로(`1024 → 1240 → 1280 → 1536` 순서 확인), `2xl` 등 상위 브레이크포인트를 덮어쓰는 캐스케이드 사고는 발생하지 않는다. `screens` 전체를 재정의할 필요가 없다.
|
||||
- **주의**: `lg`(1024px)와 `desktop`(1240px)이 공존하므로, **헤더 내 데스크톱/모바일 전환은 `desktop:`으로만 통일**한다. 동일 속성에 `lg:`와 `desktop:`을 혼용하면 1024–1239 px 구간에 의도치 않은 중간 상태가 생긴다(B2가 정확히 이 사고였다).
|
||||
|
||||
### 8.7 색 토큰 알파 계약 (Color Token Alpha Contract) *(v1.9 신설)*
|
||||
|
||||
디자인 토큰은 `app/globals.css`의 CSS 커스텀 프로퍼티와 `tailwind.config.ts`의 `theme.extend.colors`가 **한 쌍으로 동작**한다. 이 쌍의 계약이 깨지면 **lint·tsc·빌드가 모두 통과하는 채로 화면만 무너진다** — 실제로 그 사고가 발생했으므로(§12.1 D-09 상세) 계약을 명문화한다.
|
||||
|
||||
**R-8.21 [필수 · 공백 구분]** `--*-rgb` 커스텀 프로퍼티의 값은 **반드시 공백 구분 3-숫자**로 정의한다(`--ink-rgb: 10 10 10;`). **쉼표 구분(`10, 10, 10`)을 금지한다.**
|
||||
- 근거: Tailwind는 `<alpha-value>`를 포함한 색을 `rgb(var(--ink-rgb) / var(--tw-text-opacity, 1))` 형태로 전개한다. 값이 쉼표 구분이면 치환 결과가 `rgb(10, 10, 10 / 1)`이 되는데, 이는 CSS Color 4의 **현대 문법(공백+슬래시)도 레거시 문법(전량 쉼표)도 아니어서 무효**다. 무효 선언은 *invalid at computed-value time* 으로 처리되어 **해당 속성이 통째로 적용되지 않는다.** 빌드는 성공하고 CSS 파일에도 규칙이 남으므로 **어떤 정적 게이트도 이를 잡지 못한다.**
|
||||
- `--ink: rgb(var(--ink-rgb));` 형태의 파생 변수는 공백 구분에서도 그대로 유효하므로 함께 유지한다.
|
||||
|
||||
**R-8.22 [필수 · `<alpha-value>` 보존]** `theme.extend.colors`의 모든 토큰은 **`rgb(var(--*-rgb) / <alpha-value>)` 형태**로 선언한다. `var(--ink)`처럼 알파 자리표시자가 없는 형태로 축약하지 않는다.
|
||||
- 근거(실측): 알파 자리표시자가 없으면 Tailwind는 슬래시 수식자 유틸리티를 **오류 없이 조용히 생략**한다. 독립 프로브 결과 — `colors.ivory = "var(--ivory)"`일 때 `bg-ivory/85`·`text-ivory/50`·`border-ivory/15`는 **출력 CSS에 단 한 줄도 생성되지 않았고**, `colors.old = "rgb(var(--ivory-rgb) / <alpha-value>)"`일 때는 `rgb(var(--ivory-rgb) / 0.85)`로 정상 생성되었다.
|
||||
- 유틸리티가 생성되지 않으면 배경은 **투명**, 테두리는 Preflight 기본색 **`#e5e7eb`**(팔레트 외 회색), 텍스트는 **부모 색 상속**으로 대체되어 **팔레트를 벗어난 화면**이 된다.
|
||||
|
||||
**R-8.23 [금지 · 토큰 우회]** 디자인 토큰으로 표현 가능한 색을 컴포넌트의 인라인 `style` 속성이나 리터럴 색값(`#ffffff` 등)으로 우회 지정하지 않는다. 인라인 `style`은 다크 모드 변형(`dark:`)보다 **항상 우선**하므로 **테마 반전이 무력화되고 대비 요구(R-9.6)가 조용히 깨진다.**
|
||||
- 활성 내비 배지처럼 대비가 임계에 가까운 요소에는 **`opacity-*` 유틸리티를 사용하지 않는다.** 요소 불투명도는 전경·배경을 함께 합성해 대비를 낮추며, 실측상 `opacity-90`만으로 4.69:1 → 4.05:1(라이트)로 떨어져 AA 미달이 된다.
|
||||
|
||||
**R-8.24 [필수 · 헤더 가로 예산]** 데스크톱 헤더(브랜드 + 내비 + 토글)의 소요 폭은 **1240 px 뷰포트의 가용 폭 1112 px과 최대 폭 상한(78 rem) 적용 시의 가용 폭 1120 px을 모두 초과하지 않아야 한다.** 헤더에 **좌우 패딩·간격·마진을 추가하는 변경은 예산 재계산을 동반**해야 하며, 예산 초과 시 내비 라벨이 2줄로 접히는 회귀(§8.6 B2)가 재발한다.
|
||||
- 실측 기준선(commit `43c75da`): 소요 **1057.6 px** / 가용 1112 px / 여유 **+54.4 px**, 내비 전량 `font-bold` 보정 시 **+22.7 px**.
|
||||
- `container-content`의 최대 폭이 78 rem(1248 px)로 **상한**이므로, 1280 px 이상에서도 가용 폭은 **1120 px에서 고정**된다. 즉 **뷰포트를 넓혀도 헤더 넘침은 해소되지 않는다** — 넓은 화면 전용 마진(`xl:mr-16` 등)은 예산을 악화시키기만 한다.
|
||||
|
||||
**R-8.25 [금지 · 콘텐츠 중복] *(v1.10 신설)*** 서로 다른 두 라우트가 **실질적으로 동일한 `<main>` 콘텐츠**를 정적 HTML로 생성해서는 안 된다.
|
||||
- 근거: 검색 엔진은 중복을 벌점 처리하지는 않으나 **어느 URL을 정본으로 삼을지 스스로 선택**하므로, 내부 링크·사이트맵이 가리키는 URL과 색인되는 URL이 어긋날 수 있다. 또한 산출물 크기가 이유 없이 늘고, 한쪽만 수정되는 **표류(drift) 결함**이 구조적으로 가능해진다.
|
||||
- 인덱스 라우트는 하위 라우트의 본문을 재사용하는 대신 **고유한 집계·요약 콘텐츠**를 갖는다(→ IA-03).
|
||||
- 불가피하게 동일 콘텐츠를 두 URL에 노출해야 한다면 **`metadata.alternates.canonical` 지정을 필수**로 하며, 그 경우에도 R-8.25의 예외임을 §12.1에 결정 항목으로 기록한다.
|
||||
- 자동 검증: **AC-41**.
|
||||
|
||||
---
|
||||
|
||||
## 9. 비기능 요구사항 (Non-Functional Requirements)
|
||||
@@ -577,6 +602,14 @@ Job `343b5808` 검증에서 본문 좌우 여백·최대 폭 미적용(B1)이,
|
||||
- [ ] AC-32 *(PJ-05)* 모든 `deliverables[].ref`가 `content/standards.ts`에 실재하는 문서 참조와 일치함을 조인 검증
|
||||
- [ ] AC-33 *(H-02 개정 · AC-02 회귀 방어)* 히어로 재구성 후에도 **연구실 소개 서술 블록 · 주요 연구 분야 그리드 · 오시는 길 · 지표 카운터 3종 · `HeroComposition`** 이 Home 정적 HTML에 **전부 존속**함을 확인
|
||||
- [ ] AC-34 *(v1.8 신설 · PJ-13·PJ-15·PJ-16)* 프로젝트 트랙의 넘침을 **브라우저 실측 수치로 판정**한다 — 데스크톱(≥1240 px)에서 `track.scrollWidth === track.clientWidth`(스크롤 잔재 **0**), 모바일·태블릿에서는 `scrollWidth > clientWidth`가 **정상 동작**이다. 어느 뷰포트에서도 **문서 레벨** 가로 스크롤은 0이어야 한다(AC-19 · R-9.15). 데스크톱 3카드 폭의 서브픽셀 거동은 브라우저 없이는 확정할 수 없으므로 **이 단정으로 판정을 이관**한다
|
||||
- [ ] AC-35 *(v1.9 신설 · R-8.21·R-8.22)* **슬래시 수식자 실효성 게이트** — 소스(`app/**`·`components/**`의 `.tsx`·`.css`)에서 `토큰/불투명도` 형태로 사용된 **모든 색 유틸리티**를 추출하고, 각각에 대응하는 규칙이 **산출 CSS(`.next/static/css/*.css`)에 실제로 존재**함을 자동 게이트로 확인한다. 1건이라도 누락되면 실패한다. *(현행 실측: 사용 26건 / 생성 0건)*
|
||||
- [ ] AC-36 *(v1.9 신설 · R-8.21)* `app/globals.css`의 모든 `--*-rgb` 정의가 **공백 구분**임을 grep 게이트로 확인한다(쉼표 포함 시 실패). 아울러 산출 CSS에 `rgb(<쉼표 목록> / …)` 패턴이 **0건**임을 확인한다
|
||||
- [ ] AC-37 *(v1.9 신설 · R-8.23 · R-9.6)* `components/Header.tsx`에 **인라인 `style` 속성과 리터럴 색값이 0건**이며, 활성 배지 계열 요소에 **`opacity-*` 유틸리티가 0건**임을 grep으로 확인한다
|
||||
- [ ] AC-38 *(v1.9 신설 · R-9.6)* 활성 내비 배지의 **전 텍스트 상태(데스크톱 라벨·번호 / 모바일 `ko`·`en`·번호)** 가 **라이트·다크 양 테마에서 4.5:1 이상**임을 실측 색값으로 계산해 확인한다. 다크 모드 배경은 `--vermillion-rgb: 235 75 66`으로 **반전**되므로 흰색 전경은 3.74:1로 미달한다 — 다크 모드 전경은 `ivory`(반전 시 `18 18 18`, 실측 5.01:1)를 사용한다
|
||||
- [ ] AC-39 *(v1.9 신설 · R-8.24 · AC-19)* 헤더 가로 예산을 **1240 / 1280 / 1440 px** 에서 확인한다 — `header.scrollWidth <= header.clientWidth`이고 내비 항목이 **2줄로 접히지 않아야** 한다(브라우저 실측 판정)
|
||||
- [ ] AC-40 *(v1.9 신설 · IA-03 · AC-25)* 사이트맵에 등재된 **모든 URL**의 산출 HTML이 `redirect()` 전용이 아니라 **`<main>` + `container-content` 를 포함한 실체 문서**임을 확인한다
|
||||
- [ ] AC-41 *(v1.10 신설 · R-8.25)* 산출 정적 HTML 전 라우트의 **`<main>` 내부 콘텐츠를 정규화 해시**해 **동일 해시가 2건 이상 존재하지 않음**을 자동 게이트로 확인한다. `canonical`이 명시된 예외 쌍만 화이트리스트로 허용한다
|
||||
- [ ] AC-42 *(v1.10 신설 · R-9.6 · AC-38 확장)* **vermillion 배경 위 텍스트 전량**이 라이트·다크 양 테마에서 4.5:1 이상임을 확인한다 — Header 활성 배지뿐 아니라 **`CategoryNav` 활성 탭**을 포함한다. *(현행 실측: `CategoryNav` 활성 탭 `bg-vermillion text-ivory`는 라이트에서 **4.16:1로 미달**)*
|
||||
|
||||
---
|
||||
|
||||
@@ -623,6 +656,7 @@ Publications(P4)는 데이터 규모(223건)와 필터·점진적 노출 요구
|
||||
| **D-06** | 본문 여백·최대 폭 컨테이너를 **어느 계층에 부여**할 것인가 | ✅ **확정** (v1.6, 2026-07-30) | — | **결정: `<main>` 일괄 부여안 기각 → 라우트 래퍼 방식 + 자동 게이트 채택.** Planner 초안(`<main>`에 `container-content`)은 1행 수정·누락 불가라는 장점이 있으나, 개발 팀장 이의제기(Job `2560771b`)대로 **full-bleed 밴드·마퀴 티커를 구조적으로 차단**한다. 래퍼 방식의 유일한 약점(신규 라우트 누락)은 **AC-25 게이트로 상쇄**되므로 래퍼 방식이 우월하다. → R-8.17~R-8.19 |
|
||||
| **D-07** | Research Project 카드의 **`Funder/Sponsor` 실데이터 확보** 여부 | 🔴 **미결** | `funder` 미노출 + 실존하는 `Standards Track` 표기 | PJ-07. `ISL-2026/` 전체를 `과제·연구비·IITP·NRF·한국연구재단·정보통신기획평가원` 등으로 검색해 **0건**이다. **임의 생성은 MG-05·R-9.17·R-9.18 위반이며 실명·실제 이메일이 게시되는 페이지이므로 절대 불가.** 기본값으로 **구현 차단은 아니며**, 지도교수가 사업명·부처·기간을 제공하면 `content/projects.ts` 재생성만으로 반영된다 |
|
||||
| **D-08** | Research Project 집합의 **정체성 범위** — 레거시 3건(IoT/CCIS/VLC)만인지, ANL 신규 과제를 포함할지 | 🟡 미결 | 레거시 3건 노출, 섹션 라벨 `Research Projects` | 레거시 3건은 IoT 시대 실적이고 현 정체성은 ANL이다. 신규 과제 추가는 지도교수 입력이 필요하다(MG-05). 레거시 3건 비노출은 PR-01에 역행하므로 배제 |
|
||||
| **D-09** | `/publications` 인덱스 라우트를 **Overview 뷰 / `redirect()` / 기본 카테고리 재렌더 / 전체 통합 뷰 + `All` 탭** 중 무엇으로 둘 것인가 | ✅ **재확정** (v1.10, 2026-07-31) | — | **결정: Overview 뷰 채택(A안).** v1.9는 C안(첫 카테고리 본문 재렌더)을 채택했으나, 개발 팀장 이의제기(Job `22b7a309`)대로 **동일 DOM 정적 HTML 2본**이 생성되어 R-8.25를 위반한다 — 지적이 타당하여 채택안을 교체한다. `redirect()`(B안)는 AC-25·AC-40과 충돌하고, `All` 탭 포함 전체 통합 뷰(D안)는 commit `22a31ff`의 탭 제거 결정을 되돌리며 223건 단일 페이지로 P-07·R-9.11과도 충돌한다. A안은 **고유 콘텐츠**(카테고리별 집계 + 최신 하이라이트)를 가지므로 중복이 원천 소멸하고, `All` 탭 없이 3개 탭 체계를 유지해 `22a31ff`의 의도를 보존한다 → IA-03 재개정 · R-8.25 · AC-41 |
|
||||
|
||||
#### D-01 상세
|
||||
|
||||
@@ -658,6 +692,67 @@ PR-04에 명시된 4개 라벨 중 **1번과 2번의 범위가 논리적으로
|
||||
|
||||
이 확정으로 §4.1 사이트맵·§7 `PubCategory`·P-01·P-03이 v1.5에서 함께 갱신되었고, 문서 상태가 `승인(Approved)`으로 전환되었다.
|
||||
|
||||
#### D-09 상세 — 색 토큰 알파 계약 붕괴와 `/publications` 리다이렉트 *(v1.9)*
|
||||
|
||||
D-09를 촉발한 것은 라우트 문제 하나가 아니라, **정적 게이트 전부를 통과하면서 화면만 무너지는 결함 계열**이 한 배치에서 두 종류 확인된 사건이다. 계약을 §8.7로 승격한 근거를 여기에 기록한다.
|
||||
|
||||
**(1) 근본 원인 — 쉼표 구분 `--*-rgb`**
|
||||
|
||||
`app/globals.css`는 토큰을 `--vermillion-rgb: 217, 52, 43;`처럼 **쉼표 구분**으로 정의해 왔다. 반면 `tailwind.config.ts`는 `rgb(var(--vermillion-rgb) / <alpha-value>)`를 선언했고, Tailwind는 이를 다음으로 전개한다.
|
||||
|
||||
```
|
||||
.bg-vermillion { --tw-bg-opacity: 1; background-color: rgb(var(--vermillion-rgb) / var(--tw-bg-opacity, 1)) }
|
||||
↓ var() 치환 후
|
||||
rgb(217, 52, 43 / 1) ← 무효
|
||||
```
|
||||
|
||||
`rgb()`는 **공백+슬래시(현대)** 또는 **전량 쉼표(레거시)** 중 하나여야 하며 혼용은 문법 위반이다. 무효 선언은 *invalid at computed-value time* 으로 폐기되므로 **토큰 배경색이 아예 적용되지 않는다.** 커밋 `7889398`("solid vermillion background")·`40b252b`("enforce high contrast white text")가 같은 증상을 반복 교정하려 한 이력이 이 진단을 뒷받침한다.
|
||||
|
||||
**(2) 부작용 — `<alpha-value>` 제거가 만든 2차 무성 결함**
|
||||
|
||||
`7889398`은 위 증상을 `colors.ivory = "var(--ivory)"` 형태로 축약해 해결했다. 단색은 정상화되었으나, **알파 자리표시자가 사라지면 Tailwind는 슬래시 수식자 유틸리티를 오류 없이 생략한다**(독립 프로브로 확인). 그 결과 소스에 남은 **26개 사용처가 전부 무효화**되었고 lint·tsc·빌드·Gate 0–7은 그대로 통과했다.
|
||||
|
||||
| 소멸한 유틸리티 계열 | 대표 사용처 | 실제 화면 |
|
||||
| :--- | :--- | :--- |
|
||||
| `bg-*/…` | `Header` `bg-ivory/85` | sticky 헤더 **배경 소실**(본문이 비쳐 지나감) |
|
||||
| `border-*/…` (15건) | `border-line/60` 등 | Preflight 기본색 **`#e5e7eb`** — 팔레트 외 회색, 다크 모드에서 특히 부각 |
|
||||
| `text-*/…` | `Footer` `text-ink/90` | 부모 색 **상속** |
|
||||
| `ring-*/…` | `ProjectPageView` `focus:ring-vermillion/50` | Tailwind 기본 **파란색** 포커스 링 |
|
||||
|
||||
**(3) 3차 결함 — 인라인 `style` 우회**
|
||||
|
||||
단색이 적용되지 않는 상태를 우회하려 활성 배지에 `style={{ backgroundColor: "var(--vermillion)", color: "#ffffff" }}`가 투입되었다. 인라인 `style`은 `dark:` 변형을 항상 이기므로 **다크 모드에서 흰색 전경이 고정**되었고, `opacity-90`이 함께 도입되어 대비가 추가로 떨어졌다. 이는 Job `b3f001de`에서 제기하고 `43c75da`에서 해소했던 R-9.6 결함의 **직접 회귀**다.
|
||||
|
||||
| 상태 | 라이트 | 다크 | 판정 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| 배지 라벨 (흰색 단색) | 4.69:1 | **3.74:1** | 다크 미달 |
|
||||
| 배지 번호·`en` (`opacity-90`) | **4.05:1** | **3.29:1** | 양 테마 미달 |
|
||||
| 참고: `43c75da`의 `dark:text-ivory` | 4.69:1 | 5.01:1 | 양 테마 충족 |
|
||||
|
||||
**(4) 확정 — 계약 복원이 스타일 보존과 일치한다**
|
||||
|
||||
| 항목 | 내용 |
|
||||
| :--- | :--- |
|
||||
| 채택 | `--*-rgb`를 **공백 구분으로 정정**(R-8.21) + `tailwind.config.ts`에 **`<alpha-value>` 복원**(R-8.22) + 인라인 `style`·`opacity-*` **제거**(R-8.23) |
|
||||
| 기각 | `<alpha-value>` 제거 상태 유지 후 26개 사용처를 각각 리터럴 색으로 치환 — 토큰 체계가 사실상 붕괴하고 다크 모드 반전이 사용처마다 깨진다 |
|
||||
| 스타일 영향 | **의도된 디자인으로의 복원이며 재디자인이 아니다.** 사용처 26곳은 원래 의도대로 렌더되고, 단색 토큰은 `7889398` 이후 상태를 그대로 유지한다 |
|
||||
| 재발 방지 | AC-35(슬래시 수식자 실효성) · AC-36(공백 구분) · AC-37(인라인 `style` 0건) · AC-38(양 테마 대비) 게이트 신설 |
|
||||
|
||||
**(5) `/publications` 라우트** *(v1.10 재확정)*
|
||||
|
||||
같은 배치에서 `All Overview` 탭 제거와 함께 `/publications`가 `redirect()`로 대체되어 **Gate 8이 실측 실패**했다(`<main> tag not found in .next/server/app/publications.html`).
|
||||
|
||||
v1.9는 **C안(첫 카테고리 본문 재렌더)** 을 채택했으나, 개발 팀장 이의제기(Job `22b7a309`)가 **정적 HTML 2본이 동일 DOM을 갖는 문제**를 지적했고 이는 타당하다. 현재 코드베이스에 `metadataBase`·`alternates.canonical`이 **전무**함을 확인했으므로(grep 0건), C안을 그대로 두면 완화 장치 없이 중복이 노출된다. 이에 **A안(Overview 뷰)** 으로 교체한다.
|
||||
|
||||
| 안 | 중복 | `22a31ff`(All 탭 제거) 의도 | AC-25/40 | 판정 |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| A · Overview 뷰 | **없음** | **보존**(탭은 3개 유지) | 충족 | ✅ **채택** |
|
||||
| B · `redirect()` 유지 | — | 보존 | **위반** | 기각 |
|
||||
| C · 첫 카테고리 재렌더 | **2본 동일** | 보존 | 충족 | 기각 (v1.9 → v1.10 교체) |
|
||||
| D · 전체 통합 + `All` 탭 | 없음 | **역행** | 충족 | 기각 |
|
||||
|
||||
한편 이의제기의 두 번째 논거(`isActive` 하이라이트 모호)는 **성립하지 않는다.** `Header.tsx`의 활성 판정은 `pathname === href || pathname.startsWith(\`${href}/\`)`이므로, `href="/publications"`는 세 하위 카테고리 경로 전부에 대해 이미 참이 된다. v1.9 계획의 3-3(예외 분기 제거)은 이 일반 규칙만으로 성립하며 별도 조치가 필요 없다. Overview에서 하위 탭이 하나도 활성이 아닌 것은 결함이 아니라 **Overview의 정상 상태**다(→ IA-03).
|
||||
|
||||
### 12.2 리스크
|
||||
|
||||
| ID | 리스크 | 영향 | 완화 방안 |
|
||||
@@ -714,3 +809,5 @@ PR-04에 명시된 4개 라벨 중 **1번과 2번의 범위가 논리적으로
|
||||
| 1.6 | 2026-07-30 | claude (planner) | 개발 팀장(Agy) 기술 이의제기(Job `2560771b`) 반영. **§8.6 레이아웃 컨테이너 아키텍처·브레이크포인트 토큰 신설**(R-8.17~R-8.20) — `<main>` 일괄 부여안을 기각하고 3계층 분리(전체 폭 `<main>` / 라우트 래퍼 `container-content` / full-bleed 밴드)를 채택, `w-screen` 탈출 기법 금지(R-8.18) 및 `desktop: 1240px` 스크린 토큰화(R-8.20, Tailwind 정렬 동작 실측 확인). **D-06 신설·확정**, **AC-25·AC-26 신설**, R-9.14 검수 뷰포트 6종으로 확대. AC-21 정적 HTML 4개 → 3개, R-8.15 표준 문서 55 → 44건 실측치 정정. Job `019b7331` |
|
||||
| 1.7 | 2026-07-30 | claude (planner) | Home Hero → Research Project PageView 전환 계획 반영. **§6.1.2 신설**(PJ-01~PJ-14) — 레거시 실재 프로젝트 3건(IoT Standards·CCIS·VLC-IoT) 확인, `period`·`deliverables`를 `content/standards.ts` 조인으로 도출하는 무창작 설계, 활성 슬라이드 전용 캐러셀 금지(PJ-08)·URL 상태 동기화 금지(PJ-09)·캐러셀 라이브러리 금지(PJ-10)·scroll-snap 기반 JS-off 동작 보장(PJ-11) 규정. **H-02 개정**(슬로건 교체 허용 + H-03·H-04·H-05·H-06·H-07·H-10 존속 명시). **AC-27~AC-33 신설**, §7 `ResearchProject`·`ProjectDeliverable` 타입 추가, **D-07·D-08 신설**. Job `6b3840a1` |
|
||||
| 1.8 | 2026-07-30 | claude (planner) | 개발 팀장(Agy) 기술 이의제기(Job `2299a6b9`) 반영. **PJ-13 개정** — 데스크톱 3카드를 flex 스크롤 트랙에서 **`grid-cols-3` 정적 3열 + `overflow-visible`** 로 전환하고 데스크톱 퍼센트 연산을 제거. **PJ-15·PJ-16 신설**(`min-w-0`+줄바꿈으로 `shrink-0` min-content 넘침 방어 / 스크롤바 은폐 시 AC-34 동반 의무), **AC-34 신설**(데스크톱 `scrollWidth === clientWidth` 실측 판정). 이의제기의 근거 수식은 정정 — `calc(33.333%-16px)`은 초과가 아니라 **0.0125 px 미달**이며, 대안 B의 분수식은 여유가 0이어서 해당 실패 모드에 오히려 더 취약하다. Tailwind가 `calc` 연산자 공백을 자동 삽입함과 `grid-cols-3`의 `minmax(0,1fr)` 전개·미디어 쿼리 출력 순서를 실측 확인. Job `294570ab` |
|
||||
| 1.9 | 2026-07-31 | claude (planner) | 스타일 관련 누적 커밋 23건(`43c75da..b7f2e05`) 검증 결과 반영. **§8.7 색 토큰 알파 계약 신설**(R-8.21~R-8.24) — 쉼표 구분 `--*-rgb`가 `rgb(a, b, c / α)` 무효 선언을 만들어 토큰 색이 적용되지 않던 근본 원인을 규명하고, 그 우회로 도입된 `<alpha-value>` 제거가 **슬래시 수식자 유틸리티 26건을 무성 소멸**시킴을 독립 프로브로 실측 확인. 인라인 `style`·`opacity-*` 토큰 우회 금지(R-8.23)와 헤더 가로 예산(R-8.24, 1240 px 기준 −69.3 px 초과 산정) 규정. **D-09 신설·확정**(`/publications` `redirect()` 기각 → 기본 카테고리 화면), **IA-03 개정**, **AC-35~AC-40 신설**. Job `9a79e302` |
|
||||
| 1.10 | 2026-07-31 | claude (planner) | 개발 팀장(Agy) 기술 이의제기(Job `22b7a309`) 반영. **D-09 재확정** — v1.9의 C안(첫 카테고리 본문 재렌더)이 **동일 DOM 정적 HTML 2본**을 만든다는 지적이 타당하여 **A안(Overview 뷰)** 으로 교체하고, `All` 탭 재도입(D안)은 commit `22a31ff` 결정 역행·P-07 충돌로 기각. **R-8.25 신설**(라우트 간 콘텐츠 중복 금지 + canonical 예외 절차), **AC-41 신설**(`<main>` 정규화 해시 중복 0건), **IA-03 재개정**. 이의제기의 `isActive` 논거는 정정 — `pathname.startsWith(\`${href}/\`)` 일반 규칙이 하위 카테고리를 이미 포괄하므로 모호성이 없음을 코드로 확인. 아울러 별건으로 `CategoryNav` 활성 탭(`bg-vermillion text-ivory`)이 **라이트 테마 4.16:1로 R-9.6 미달**임을 발견해 **AC-42 신설**. Job `6a6094c6` |
|
||||
|
||||
@@ -39,16 +39,16 @@
|
||||
--font-sans: 'Geist', 'Pretendard', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Apple SD Gothic Neo', 'Malgun Gothic', sans-serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
||||
|
||||
--ink-rgb: 10, 10, 10;
|
||||
--ink-soft-rgb: 38, 36, 32;
|
||||
--ink-mute-rgb: 107, 101, 91;
|
||||
--ivory-rgb: 245, 241, 232;
|
||||
--paper-rgb: 236, 230, 214;
|
||||
--line-rgb: 216, 209, 192;
|
||||
--vermillion-rgb: 217, 52, 43;
|
||||
--vermillion-dark-rgb: 168, 35, 28;
|
||||
--cobalt-rgb: 27, 58, 138;
|
||||
--cobalt-dark-rgb: 18, 40, 99;
|
||||
--ink-rgb: 10 10 10;
|
||||
--ink-soft-rgb: 38 36 32;
|
||||
--ink-mute-rgb: 107 101 91;
|
||||
--ivory-rgb: 245 241 232;
|
||||
--paper-rgb: 236 230 214;
|
||||
--line-rgb: 216 209 192;
|
||||
--vermillion-rgb: 217 52 43;
|
||||
--vermillion-dark-rgb: 168 35 28;
|
||||
--cobalt-rgb: 27 58 138;
|
||||
--cobalt-dark-rgb: 18 40 99;
|
||||
|
||||
--ink: rgb(var(--ink-rgb));
|
||||
--ink-soft: rgb(var(--ink-soft-rgb));
|
||||
@@ -72,16 +72,16 @@ html.dark,
|
||||
html[data-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
|
||||
--ink-rgb: 245, 241, 232;
|
||||
--ink-soft-rgb: 216, 209, 192;
|
||||
--ink-mute-rgb: 160, 153, 142;
|
||||
--ivory-rgb: 18, 18, 18;
|
||||
--paper-rgb: 30, 28, 26;
|
||||
--line-rgb: 51, 48, 43;
|
||||
--vermillion-rgb: 235, 75, 66;
|
||||
--vermillion-dark-rgb: 255, 107, 97;
|
||||
--cobalt-rgb: 77, 119, 255;
|
||||
--cobalt-dark-rgb: 112, 148, 255;
|
||||
--ink-rgb: 245 241 232;
|
||||
--ink-soft-rgb: 216 209 192;
|
||||
--ink-mute-rgb: 160 153 142;
|
||||
--ivory-rgb: 18 18 18;
|
||||
--paper-rgb: 30 28 26;
|
||||
--line-rgb: 51 48 43;
|
||||
--vermillion-rgb: 235 75 66;
|
||||
--vermillion-dark-rgb: 255 107 97;
|
||||
--cobalt-rgb: 77 119 255;
|
||||
--cobalt-dark-rgb: 112 148 255;
|
||||
|
||||
--grain-color: rgba(245, 241, 232, 0.03);
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ export default function HomePage() {
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-serif text-3xl text-cobalt font-normal">
|
||||
220+
|
||||
{Math.floor((publications.length + patents.length) / 10) * 10}+
|
||||
</div>
|
||||
<div className="font-mono text-xs text-ink-mute uppercase tracking-wider mt-1">
|
||||
Publications
|
||||
|
||||
@@ -5,7 +5,6 @@ import { PubCategory } from "../../../content/types";
|
||||
|
||||
interface CategoryNavProps {
|
||||
currentCategory?: PubCategory;
|
||||
totalCount?: number;
|
||||
}
|
||||
|
||||
export function CategoryNav({ currentCategory }: CategoryNavProps) {
|
||||
@@ -20,7 +19,7 @@ export function CategoryNav({ currentCategory }: CategoryNavProps) {
|
||||
href={`/publications/${cat.slug}`}
|
||||
className={`px-4 py-2 rounded text-xs font-mono tracking-wider transition-colors ${
|
||||
isActive
|
||||
? "bg-vermillion text-ivory font-bold"
|
||||
? "bg-vermillion text-white dark:text-ivory font-bold"
|
||||
: "bg-paper text-ink border border-line hover:border-vermillion"
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,128 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { CategoryNav } from "./_components/CategoryNav";
|
||||
import { PUB_CATEGORIES } from "../../content/categories";
|
||||
import { publications, patents } from "../../content/publications";
|
||||
|
||||
export const metadata = {
|
||||
title: "Publications Overview | ANL",
|
||||
description: "AI 에이전트 네트워크 연구실(ANL) 연구 실적 개요 및 카테고리별 집계",
|
||||
};
|
||||
|
||||
export default function PublicationsIndexPage() {
|
||||
redirect("/publications/intl-journal-conf");
|
||||
const totalPubCount = publications.length + patents.length;
|
||||
|
||||
// Sort publication records by publishedAt descending to select top 10 highlights
|
||||
const top10Highlights = [...publications]
|
||||
.sort((a, b) => (b.publishedAt || "").localeCompare(a.publishedAt || ""))
|
||||
.slice(0, 10);
|
||||
|
||||
const getCount = (slug: string) => {
|
||||
if (slug === "patent") return patents.length;
|
||||
return publications.filter((p) => p.category === slug).length;
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="container-content space-y-8 md:space-y-12 pt-10 md:pt-16 pb-16 md:pb-24">
|
||||
{/* Header section */}
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="h-px w-8 bg-vermillion" />
|
||||
<span className="font-mono text-xs font-bold uppercase tracking-widest text-vermillion">
|
||||
BIBLIOGRAPHY & INTELLECTUAL PROPERTY
|
||||
</span>
|
||||
</div>
|
||||
<h1 className="headline-ko text-3xl sm:text-4xl text-ink font-bold">
|
||||
연구 실적 개요
|
||||
</h1>
|
||||
<p className="text-sm sm:text-base text-ink-mute max-w-3xl leading-relaxed">
|
||||
AI 에이전트 네트워크 연구실(ANL)의 학술 논문({publications.length}건)과 특허({patents.length}건)를 포함한 총 {totalPubCount}건의 연구 성과 집계 및 대표 실적 목록입니다.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Category Navigation */}
|
||||
<CategoryNav />
|
||||
|
||||
{/* Category Summary Cards */}
|
||||
<section className="grid gap-6 md:grid-cols-3">
|
||||
{PUB_CATEGORIES.map((cat) => {
|
||||
const count = getCount(cat.slug);
|
||||
return (
|
||||
<div
|
||||
key={cat.slug}
|
||||
className="flex flex-col justify-between border border-line bg-paper p-6 rounded-lg transition-colors hover:border-vermillion group"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<span className="font-mono text-xs text-ink-mute uppercase tracking-wider">
|
||||
Category
|
||||
</span>
|
||||
<h2 className="headline-ko text-xl text-ink font-bold group-hover:text-vermillion transition-colors">
|
||||
{cat.label}
|
||||
</h2>
|
||||
<p className="text-xs text-ink-mute leading-relaxed">
|
||||
{cat.desc}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-6 pt-4 border-t border-line flex items-baseline justify-between">
|
||||
<span className="font-display text-3xl font-bold text-ink">
|
||||
{count}
|
||||
<span className="text-xs font-sans text-ink-mute ml-1 font-normal">건</span>
|
||||
</span>
|
||||
<Link
|
||||
href={`/publications/${cat.slug}`}
|
||||
className="inline-flex items-center text-xs font-bold text-vermillion hover:underline"
|
||||
>
|
||||
전체 보기 →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
|
||||
{/* Top 10 Highlights Section */}
|
||||
<section className="space-y-6 pt-6 border-t border-line">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="headline-ko text-2xl font-bold text-ink">
|
||||
최신 대표 실적 (Top 10 Highlights)
|
||||
</h2>
|
||||
<span className="font-mono text-xs text-ink-mute">
|
||||
RECENT HIGHLIGHTS
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-line border border-line bg-ivory rounded-lg overflow-hidden">
|
||||
{top10Highlights.map((pub, idx) => (
|
||||
<article key={`${pub.category}-${idx}`} className="p-5 hover:bg-paper/50 transition-colors space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs font-mono text-ink-mute">
|
||||
<span className="px-2 py-0.5 rounded bg-paper border border-line text-ink font-bold">
|
||||
{pub.publishedAt?.substring(0, 7) || "Recent"}
|
||||
</span>
|
||||
<span>·</span>
|
||||
<span className="capitalize">{pub.category.replace(/-/g, " ")}</span>
|
||||
{pub.doi && (
|
||||
<>
|
||||
<span>·</span>
|
||||
<span className="text-vermillion">DOI: {pub.doi}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="font-semibold text-base text-ink leading-snug">
|
||||
{pub.title}
|
||||
</h3>
|
||||
{pub.authors && (
|
||||
<p className="text-xs text-ink-mute">
|
||||
{pub.authors}
|
||||
</p>
|
||||
)}
|
||||
{pub.venue && (
|
||||
<p className="text-xs italic text-ink-soft">
|
||||
{pub.venue}
|
||||
</p>
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import ThemeToggle from "./ThemeToggle";
|
||||
const navItems = [
|
||||
{ href: "/", ko: "홈", en: "Home", no: "01" },
|
||||
{ href: "/members", ko: "구성원", en: "Members", no: "02" },
|
||||
{ href: "/publications/intl-journal-conf", ko: "논문", en: "Publications", no: "03" },
|
||||
{ href: "/publications", ko: "논문", en: "Publications", no: "03" },
|
||||
{ href: "/lectures", ko: "강의", en: "Lectures", no: "04" },
|
||||
{ href: "/standardization", ko: "표준화", en: "Standardization", no: "05" },
|
||||
];
|
||||
@@ -22,13 +22,13 @@ export default function Header() {
|
||||
pathname
|
||||
? href === "/"
|
||||
? pathname === "/"
|
||||
: pathname === href || pathname.startsWith(`${href}/`) || (href.startsWith("/publications") && pathname.startsWith("/publications"))
|
||||
: pathname === href || pathname.startsWith(`${href}/`)
|
||||
: false;
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 border-b border-ink bg-ivory/85 backdrop-blur py-2 sm:py-3">
|
||||
<header className="sticky top-0 z-50 border-b border-ink bg-ivory/85 backdrop-blur">
|
||||
{/* Journal issue strip — magazine masthead feel. */}
|
||||
<div className="hidden border-b border-line pb-2 mb-2 desktop:block">
|
||||
<div className="hidden border-b border-line desktop:block">
|
||||
<div className="container-content flex items-center justify-between py-1.5">
|
||||
<span className="kicker">KNU/CSE/ANL</span>
|
||||
{/* CUSTOMIZATION HOOK: masthead tagline */}
|
||||
@@ -39,11 +39,11 @@ export default function Header() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container-content flex min-h-[4.5rem] items-center justify-between gap-8 py-2 sm:py-3">
|
||||
{/* Brand logo — explicit right margin to guarantee clear separation from 01 HOME */}
|
||||
<div className="container-content flex h-[4.25rem] items-center justify-between gap-4">
|
||||
{/* Brand logo */}
|
||||
<Link
|
||||
href="/"
|
||||
className="group flex flex-col leading-none shrink-0 mr-8 xl:mr-16"
|
||||
className="group flex flex-col leading-none shrink-0"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<span className="headline-ko text-lg text-ink sm:text-xl">
|
||||
@@ -55,7 +55,7 @@ export default function Header() {
|
||||
</Link>
|
||||
|
||||
{/* Desktop nav — small caps, wide tracking (visible at >= 1240px) */}
|
||||
<div className="hidden items-center gap-6 desktop:flex ml-auto">
|
||||
<div className="hidden items-center gap-4 desktop:flex">
|
||||
<nav className="flex items-center gap-2" aria-label="주 메뉴">
|
||||
{navItems.map((item) => {
|
||||
const active = isActive(item.href);
|
||||
@@ -64,28 +64,20 @@ export default function Header() {
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
aria-current={active ? "page" : undefined}
|
||||
style={
|
||||
className={`group relative flex items-baseline gap-1.5 text-[0.78rem] font-semibold uppercase tracking-[0.18em] transition-all px-3 py-1.5 rounded-md ${
|
||||
active
|
||||
? { backgroundColor: "var(--vermillion)", color: "#ffffff" }
|
||||
: undefined
|
||||
}
|
||||
className={`group relative flex items-baseline gap-1.5 text-[0.78rem] font-bold uppercase tracking-[0.18em] transition-all px-3.5 py-1.5 rounded-md ${
|
||||
active
|
||||
? "shadow-sm font-bold"
|
||||
? "bg-vermillion text-white dark:text-ivory shadow-sm font-bold"
|
||||
: "text-ink hover:text-vermillion hover:bg-paper/60"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
style={active ? { color: "#ffffff" } : undefined}
|
||||
className={`font-display text-[0.62rem] ${
|
||||
active ? "opacity-90 font-bold" : "text-ink-mute"
|
||||
active ? "text-white dark:text-ivory" : "text-ink-mute"
|
||||
}`}
|
||||
>
|
||||
{item.no}
|
||||
</span>
|
||||
<span style={active ? { color: "#ffffff" } : undefined}>
|
||||
{item.en}
|
||||
</span>
|
||||
<span>{item.en}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
@@ -132,37 +124,25 @@ export default function Header() {
|
||||
href={item.href}
|
||||
onClick={() => setOpen(false)}
|
||||
aria-current={active ? "page" : undefined}
|
||||
style={
|
||||
active
|
||||
? { backgroundColor: "var(--vermillion)", color: "#ffffff" }
|
||||
: undefined
|
||||
}
|
||||
className={`flex items-baseline justify-between px-4 py-3.5 transition-colors rounded-md my-1 ${
|
||||
active
|
||||
? "font-bold shadow-sm"
|
||||
? "bg-vermillion text-white dark:text-ivory font-bold shadow-sm"
|
||||
: "text-ink hover:bg-paper/60"
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-baseline gap-3">
|
||||
<span
|
||||
style={active ? { color: "#ffffff" } : undefined}
|
||||
className={`font-display text-xs ${
|
||||
active ? "opacity-90 font-bold" : "text-ink-mute"
|
||||
active ? "text-white dark:text-ivory" : "text-ink-mute"
|
||||
}`}
|
||||
>
|
||||
{item.no}
|
||||
</span>
|
||||
<span
|
||||
style={active ? { color: "#ffffff" } : undefined}
|
||||
className="headline-ko text-lg font-bold"
|
||||
>
|
||||
{item.ko}
|
||||
</span>
|
||||
<span className="headline-ko text-lg">{item.ko}</span>
|
||||
</span>
|
||||
<span
|
||||
style={active ? { color: "#ffffff" } : undefined}
|
||||
className={`text-[0.7rem] font-semibold uppercase tracking-[0.18em] ${
|
||||
active ? "opacity-90 font-bold" : "text-ink-mute"
|
||||
active ? "text-white dark:text-ivory" : "text-ink-mute"
|
||||
}`}
|
||||
>
|
||||
{item.en}
|
||||
|
||||
@@ -389,77 +389,77 @@ export const publications: Publication[] = [
|
||||
},
|
||||
{
|
||||
"category": "intl-journal-conf",
|
||||
"title": "Mobility Management Requirements and Framework for Systems Beyond IMT-2000\",",
|
||||
"title": "Mobility Management Requirements and Framework for Systems Beyond IMT-2000",
|
||||
"venue": "Journal of Communications and Networks",
|
||||
"volume": "Vol. 7, No. 2, pp. 171~177, June 2005",
|
||||
"publishedAt": "2005-06"
|
||||
},
|
||||
{
|
||||
"category": "intl-journal-conf",
|
||||
"title": "Transport Layer Mobility Support Utilizing Link Signal Strength Information\",",
|
||||
"title": "Transport Layer Mobility Support Utilizing Link Signal Strength Information",
|
||||
"venue": "IEICE Transactions on Communications",
|
||||
"volume": "Vol. E87-B, No. 9, pp. 2548~2556, September 2004",
|
||||
"publishedAt": "2004-09"
|
||||
},
|
||||
{
|
||||
"category": "intl-journal-conf",
|
||||
"title": "Limiting the Length of BET for Tunnel-Based IP Fast Handover\",",
|
||||
"title": "Limiting the Length of BET for Tunnel-Based IP Fast Handover",
|
||||
"venue": "IEICE Transactions on Fundamentals of Electronics Communications and Computer Sciences",
|
||||
"volume": "Vol. E87-A, No. 6, pp. 1527~1530, June 2004",
|
||||
"publishedAt": "2004-06"
|
||||
},
|
||||
{
|
||||
"category": "intl-journal-conf",
|
||||
"title": "mSCTP for Soft Handover in Transport Layer\",",
|
||||
"title": "mSCTP for Soft Handover in Transport Layer",
|
||||
"venue": "IEEE Communications Letters",
|
||||
"volume": "Vol. 8, No. 3, pp. 189~191, March 2004",
|
||||
"publishedAt": "2004-03"
|
||||
},
|
||||
{
|
||||
"category": "intl-journal-conf",
|
||||
"title": "Configuration of ACK Trees for Multicast Transport Protocols\",",
|
||||
"title": "Configuration of ACK Trees for Multicast Transport Protocols",
|
||||
"venue": "ETRI Journal",
|
||||
"volume": "Vol. 23, No. 3, pp. 111~120, September 2001",
|
||||
"publishedAt": "2001-09"
|
||||
},
|
||||
{
|
||||
"category": "intl-journal-conf",
|
||||
"title": "Assignment of ADM Rings and DCS Mesh in Telecommunication Networks\",",
|
||||
"title": "Assignment of ADM Rings and DCS Mesh in Telecommunication Networks",
|
||||
"venue": "Journal of the O.R. Society",
|
||||
"volume": "Vol. 52, No. 4, pp. 440~448, April 2001",
|
||||
"publishedAt": "2001-04"
|
||||
},
|
||||
{
|
||||
"category": "intl-journal-conf",
|
||||
"title": "Multicast Delivery Based on Unicast and Subnet Multicast\",",
|
||||
"title": "Multicast Delivery Based on Unicast and Subnet Multicast",
|
||||
"venue": "IEEE Communications Letters",
|
||||
"volume": "Vol. 5, No. 4, pp. 181~183, April 2001",
|
||||
"publishedAt": "2001-04"
|
||||
},
|
||||
{
|
||||
"category": "intl-journal-conf",
|
||||
"title": "Dynamic Bandwidth Allocation Scheme for Multiple Real-time VBR Videos over ATM Networks\",",
|
||||
"title": "Dynamic Bandwidth Allocation Scheme for Multiple Real-time VBR Videos over ATM Networks",
|
||||
"venue": "Telecommunications Systems",
|
||||
"volume": "Vol. 15, pp.359~380, December 2000",
|
||||
"publishedAt": "2000-12"
|
||||
},
|
||||
{
|
||||
"category": "intl-journal-conf",
|
||||
"title": "Minimizing Cost and Delay in Shared Multicast Trees\",",
|
||||
"title": "Minimizing Cost and Delay in Shared Multicast Trees",
|
||||
"venue": "ETRI Journal",
|
||||
"volume": "Vol. 22, No. 1, pp.30~37, March 2000",
|
||||
"publishedAt": "2000-03"
|
||||
},
|
||||
{
|
||||
"category": "intl-journal-conf",
|
||||
"title": "Non-Core Based Shared Tree Architecture for IP Multicasting\",",
|
||||
"title": "Non-Core Based Shared Tree Architecture for IP Multicasting",
|
||||
"venue": "Electronics Letters",
|
||||
"volume": "Vol. 35, No. 11, pp. 872~873, May 1999",
|
||||
"publishedAt": "1999-05"
|
||||
},
|
||||
{
|
||||
"category": "intl-journal-conf",
|
||||
"title": "A Design of the Self-Healing ATM Networks Based on the Backup Virtual Path,",
|
||||
"title": "A Design of the Self-Healing ATM Networks Based on the Backup Virtual Path",
|
||||
"venue": "Computers and Operations Research",
|
||||
"volume": "Vol. 25, No. 7/8, pp. 595~609, July 1998",
|
||||
"publishedAt": "1998-07"
|
||||
@@ -473,7 +473,7 @@ export const publications: Publication[] = [
|
||||
},
|
||||
{
|
||||
"category": "intl-journal-conf",
|
||||
"title": "A Tabu Search for the Survivable Fiber Optic Communication Network Design,",
|
||||
"title": "A Tabu Search for the Survivable Fiber Optic Communication Network Design",
|
||||
"venue": "Computers and Industrial Engineering",
|
||||
"volume": "Vol. 28, Issue 4, pp. 689~700, October 1995",
|
||||
"publishedAt": "1995-10"
|
||||
@@ -1096,7 +1096,7 @@ export const publications: Publication[] = [
|
||||
},
|
||||
{
|
||||
"category": "domestic-journal-conf",
|
||||
"title": "멀티캐스트 전송을 위한 오류제어 기법의 분류\",",
|
||||
"title": "멀티캐스트 전송을 위한 오류제어 기법의 분류",
|
||||
"venue": "ETRI 전자통신동향분석",
|
||||
"volume": "99년 6월호, pp. 76 - 84, 1999.",
|
||||
"publishedAt": "1999",
|
||||
@@ -1476,7 +1476,7 @@ export const patents: Patent[] = [
|
||||
},
|
||||
{
|
||||
"category": "patent",
|
||||
"title": "ESL 신호를 이용하여 위치 기반 서비스를 제공하는 스마트 장치,",
|
||||
"title": "ESL 신호를 이용하여 위치 기반 서비스를 제공하는 스마트 장치",
|
||||
"inventors": "고석주, 김지인, 이민형, 김종근, 박지수, 조정근, 이승일, 서대화",
|
||||
"applicationNo": "2014-0180036",
|
||||
"applicationAt": "2014-12-15",
|
||||
|
||||
@@ -117,7 +117,9 @@ def parse_members():
|
||||
clean_item = re.sub(r'<.*?>', '', clean_item)
|
||||
clean_item = clean_item.strip().replace('\n', ' ')
|
||||
clean_item = re.sub(r'[:&\s]+$', '', clean_item)
|
||||
if "Seok-Joo Koh" in clean_item or "고석주" in clean_item or "Professor" in clean_item:
|
||||
if not clean_item:
|
||||
continue
|
||||
if re.search(r'^Seok-Joo Koh\b', clean_item) or "고석주" in clean_item:
|
||||
en = "Seok-Joo Koh"
|
||||
ko = "고석주"
|
||||
degree = "Professor"
|
||||
@@ -209,7 +211,7 @@ def parse_publications():
|
||||
m_title = re.search(r'<U>(.*?)</U>', item, flags=re.S | re.I)
|
||||
if m_title:
|
||||
raw_t = re.sub(r'<br/?>', '', m_title.group(1), flags=re.I).replace('\n', ' ').strip()
|
||||
title = re.sub(r'^(?:"|“|”|\s)+|(?:"|“|”|\s)+$', '', raw_t).strip()
|
||||
title = re.sub(r'^[\s"“”]+|[\s"“”,]+$', '', raw_t).strip()
|
||||
else:
|
||||
title = ""
|
||||
|
||||
@@ -236,7 +238,7 @@ def parse_publications():
|
||||
m_title = re.search(r'<U>(.*?)</U>', item, flags=re.S | re.I)
|
||||
if m_title:
|
||||
raw_t = re.sub(r'<br/?>', '', m_title.group(1), flags=re.I).replace('\n', ' ').strip()
|
||||
title = re.sub(r'^(?:"|“|”|\s)+|(?:"|“|”|\s)+$', '', raw_t).strip()
|
||||
title = re.sub(r'^[\s"“”]+|[\s"“”,]+$', '', raw_t).strip()
|
||||
else:
|
||||
title = ""
|
||||
|
||||
@@ -266,7 +268,7 @@ def parse_publications():
|
||||
lines = [l.strip() for l in clean.split('\n') if l.strip()]
|
||||
if not lines:
|
||||
continue
|
||||
title = lines[0]
|
||||
title = re.sub(r'^[\s"“”]+|[\s"“”,]+$', '', lines[0]).strip()
|
||||
inventors = ""
|
||||
app_no = ""
|
||||
app_date = None
|
||||
|
||||
@@ -54,6 +54,12 @@ def verify():
|
||||
if "63246" not in d["ref"]:
|
||||
errors.append(f"AC-32 Violation: CCIS project deliverable '{d['ref']}' does not belong to CCIS WG (IEC 63246)")
|
||||
|
||||
# N1: Title artifact check (No trailing quotes or commas in publication titles)
|
||||
for p in intl_p + dom_p + pat_p:
|
||||
t = p.get("title", "")
|
||||
if re.search(r'["“”]|,$', t):
|
||||
errors.append(f"N1 Title Artifact Violation in publication '{p.get('id')}': title '{t}' contains quotes or trailing comma")
|
||||
|
||||
# 1. Count checks
|
||||
if len(active_m) != 16:
|
||||
errors.append(f"Active Members count mismatch: got {len(active_m)}, expected 16")
|
||||
|
||||
@@ -159,18 +159,15 @@ def test_unittest_suite():
|
||||
return True
|
||||
|
||||
def test_layout_architecture():
|
||||
print("\n--- Gate 8: Layout Architecture & No-Escape Gate (AC-25 / AC-26 / R-8.17) ---")
|
||||
print("\n--- Gate 8: Layout Architecture & No-Escape Gate (AC-25 / AC-26 / AC-40 / AC-41 / R-8.17) ---")
|
||||
|
||||
# 1) Check HTML SSG pages for container-content inside main
|
||||
routes_to_check = [
|
||||
os.path.join(".next", "server", "app", "index.html"),
|
||||
os.path.join(".next", "server", "app", "members.html"),
|
||||
os.path.join(".next", "server", "app", "publications.html"),
|
||||
os.path.join(".next", "server", "app", "lectures.html"),
|
||||
os.path.join(".next", "server", "app", "standardization.html"),
|
||||
]
|
||||
# 1) Check HTML SSG pages for container-content inside main across ALL SSG routes (C2, AC-25)
|
||||
routes_to_check = EXPECTED_SSG_ROUTES
|
||||
|
||||
main_hashes = {}
|
||||
for rel_p in routes_to_check:
|
||||
if not rel_p.endswith(".html"):
|
||||
continue
|
||||
full_p = os.path.join(REFER_DIR, rel_p)
|
||||
if not os.path.exists(full_p):
|
||||
print(f"❌ Missing SSG route HTML: {rel_p}")
|
||||
@@ -178,46 +175,78 @@ def test_layout_architecture():
|
||||
with open(full_p, "r", encoding="utf-8") as f:
|
||||
html = f.read()
|
||||
|
||||
# Verify <main tag does NOT contain max-w- or px-
|
||||
main_match = re.search(r'<main\s+class="([^"]*)"', html)
|
||||
# Verify <main> tag exists and does NOT contain max-w- or px-
|
||||
main_match = re.search(r'<main(?:\s+[^>]*)?>(.*?)</main>', html, flags=re.S)
|
||||
if not main_match:
|
||||
print(f"❌ <main> tag not found in {rel_p}")
|
||||
return False
|
||||
main_cls = main_match.group(1)
|
||||
if "max-w-" in main_cls or "px-" in main_cls or "container-content" in main_cls:
|
||||
print(f"❌ <main> tag in {rel_p} restricts width or padding ({main_cls}). Should be w-full flex-1 only.")
|
||||
|
||||
main_inner = main_match.group(1)
|
||||
|
||||
# C1: Verify child wrapper inside <main> has container-content
|
||||
if "container-content" not in main_inner and "_not-found" not in rel_p and "404" not in rel_p:
|
||||
print(f"❌ container-content missing inside <main> in {rel_p}")
|
||||
return False
|
||||
|
||||
# Verify child wrapper has container-content
|
||||
if "container-content" not in html:
|
||||
print(f"❌ container-content missing in {rel_p}")
|
||||
return False
|
||||
# AC-41: Check for duplicate <main> normalized content hashes (except 404 pages)
|
||||
if "404" not in rel_p and "_not-found" not in rel_p:
|
||||
norm_content = re.sub(r'\s+', '', main_inner)
|
||||
if norm_content in main_hashes:
|
||||
print(f"❌ AC-41 Violation: Duplicate <main> content found between {rel_p} and {main_hashes[norm_content]}")
|
||||
return False
|
||||
main_hashes[norm_content] = rel_p
|
||||
|
||||
# 2) Check source for forbidden escaped arbitrary classes (AC-26)
|
||||
forbidden_terms = ["w-screen", "-mx-[50vw]", "min-[1240px]:"]
|
||||
for root, _, files in os.walk(os.path.join(REFER_DIR, "app")):
|
||||
for fname in files:
|
||||
if fname.endswith((".ts", ".tsx")):
|
||||
fpath = os.path.join(root, fname)
|
||||
with open(fpath, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
for term in forbidden_terms:
|
||||
if term in content:
|
||||
print(f"❌ Forbidden term '{term}' found in {fpath}")
|
||||
return False
|
||||
for root_dir in [os.path.join(REFER_DIR, "app"), os.path.join(REFER_DIR, "components")]:
|
||||
for root, _, files in os.walk(root_dir):
|
||||
for fname in files:
|
||||
if fname.endswith((".ts", ".tsx")):
|
||||
fpath = os.path.join(root, fname)
|
||||
with open(fpath, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
for term in forbidden_terms:
|
||||
if term in content:
|
||||
print(f"❌ Forbidden term '{term}' found in {fpath}")
|
||||
return False
|
||||
|
||||
for root, _, files in os.walk(os.path.join(REFER_DIR, "components")):
|
||||
for fname in files:
|
||||
if fname.endswith((".ts", ".tsx")):
|
||||
fpath = os.path.join(root, fname)
|
||||
with open(fpath, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
for term in forbidden_terms:
|
||||
if term in content:
|
||||
print(f"❌ Forbidden term '{term}' found in {fpath}")
|
||||
return False
|
||||
print("✅ Gate 8 PASSED: Layout 3-tier architecture, route uniqueness (AC-41), and AC-25/AC-26 no-escape rule verified")
|
||||
return True
|
||||
|
||||
print("✅ Gate 8 PASSED: Layout 3-tier architecture and AC-25/AC-26 no-escape rule verified")
|
||||
def test_token_contracts():
|
||||
print("\n--- Gate 9: Color Token Alpha Contract & Header Style Gate (AC-35 / AC-36 / AC-37) ---")
|
||||
|
||||
# AC-36: Verify zero commas in --*-rgb definitions in globals.css
|
||||
globals_css = os.path.join(REFER_DIR, "app", "globals.css")
|
||||
with open(globals_css, "r", encoding="utf-8") as f:
|
||||
css_text = f.read()
|
||||
|
||||
rgb_lines = re.findall(r'--[a-z-]+-rgb:\s*([^;]+);', css_text)
|
||||
for line in rgb_lines:
|
||||
if "," in line:
|
||||
print(f"❌ AC-36 Violation: Comma found in --*-rgb definition '{line}' in globals.css")
|
||||
return False
|
||||
|
||||
# AC-35: Verify tailwind.config.ts has <alpha-value> for color tokens
|
||||
tw_config = os.path.join(REFER_DIR, "tailwind.config.ts")
|
||||
with open(tw_config, "r", encoding="utf-8") as f:
|
||||
tw_text = f.read()
|
||||
if "<alpha-value>" not in tw_text:
|
||||
print("❌ AC-35 Violation: <alpha-value> placeholder missing in tailwind.config.ts")
|
||||
return False
|
||||
|
||||
# AC-37: Verify Header.tsx has zero inline style={{ and zero opacity- in active badges
|
||||
header_path = os.path.join(REFER_DIR, "components", "Header.tsx")
|
||||
with open(header_path, "r", encoding="utf-8") as f:
|
||||
header_text = f.read()
|
||||
if "style={{" in header_text or "style={" in header_text:
|
||||
print("❌ AC-37 Violation: Inline style attribute found in Header.tsx")
|
||||
return False
|
||||
if "opacity-" in header_text:
|
||||
print("❌ AC-37 Violation: opacity- modifier found in Header.tsx active badge elements")
|
||||
return False
|
||||
|
||||
print("✅ Gate 9 PASSED: Color token alpha contract, space separation (AC-36), and Header clean styling (AC-37) verified")
|
||||
return True
|
||||
|
||||
def main():
|
||||
@@ -235,6 +264,7 @@ def main():
|
||||
success &= test_theme_tokens()
|
||||
success &= test_unittest_suite()
|
||||
success &= test_layout_architecture()
|
||||
success &= test_token_contracts()
|
||||
|
||||
print("\n" + "-" * 70)
|
||||
if success:
|
||||
|
||||
@@ -33,28 +33,28 @@ const config: Config = {
|
||||
},
|
||||
colors: {
|
||||
ink: {
|
||||
DEFAULT: "var(--ink)",
|
||||
soft: "var(--ink-soft)",
|
||||
mute: "var(--ink-mute)",
|
||||
DEFAULT: "rgb(var(--ink-rgb) / <alpha-value>)",
|
||||
soft: "rgb(var(--ink-soft-rgb) / <alpha-value>)",
|
||||
mute: "rgb(var(--ink-mute-rgb) / <alpha-value>)",
|
||||
},
|
||||
ivory: "var(--ivory)",
|
||||
paper: "var(--paper)",
|
||||
line: "var(--line)",
|
||||
ivory: "rgb(var(--ivory-rgb) / <alpha-value>)",
|
||||
paper: "rgb(var(--paper-rgb) / <alpha-value>)",
|
||||
line: "rgb(var(--line-rgb) / <alpha-value>)",
|
||||
vermillion: {
|
||||
DEFAULT: "var(--vermillion)",
|
||||
dark: "var(--vermillion-dark)",
|
||||
DEFAULT: "rgb(var(--vermillion-rgb) / <alpha-value>)",
|
||||
dark: "rgb(var(--vermillion-dark-rgb) / <alpha-value>)",
|
||||
},
|
||||
cobalt: {
|
||||
DEFAULT: "var(--cobalt)",
|
||||
dark: "var(--cobalt-dark)",
|
||||
DEFAULT: "rgb(var(--cobalt-rgb) / <alpha-value>)",
|
||||
dark: "rgb(var(--cobalt-dark-rgb) / <alpha-value>)",
|
||||
},
|
||||
// Back-compat aliases so any stray legacy class still resolves.
|
||||
brand: {
|
||||
DEFAULT: "var(--vermillion)",
|
||||
dark: "var(--vermillion-dark)",
|
||||
light: "var(--cobalt)",
|
||||
DEFAULT: "rgb(var(--vermillion-rgb) / <alpha-value>)",
|
||||
dark: "rgb(var(--vermillion-dark-rgb) / <alpha-value>)",
|
||||
light: "rgb(var(--cobalt-rgb) / <alpha-value>)",
|
||||
},
|
||||
accent: { DEFAULT: "var(--cobalt)" },
|
||||
accent: { DEFAULT: "rgb(var(--cobalt-rgb) / <alpha-value>)" },
|
||||
},
|
||||
fontFamily: {
|
||||
// English display serif — magazine cover scale.
|
||||
|
||||
Reference in New Issue
Block a user