static-portfolio-build — 정적 다중 페이지 포트폴리오
기술적 경계선: HTML5 + CSS3 + 네이티브 JS만 사용. Bootstrap/Tailwind 같은 UI 프레임워크 금지, 외부 CDN 글꼴 금지. 기본 색상 ≤3개, 글꼴 ≤2개, 모든 이미지에
alt속성 필수(SVG는role="img"+aria-label사용). 첫 화면 로딩 3G ≤3초(1.6Mbps / RTT 400ms 추정).
1. 아키텍처: 데이터 기반, 한 곳에서 유지보수
각 HTML에 수동으로 포트폴리오 내용을 작성하지 마세요. JSON 데이터 + 렌더링 레이어로 추출:
works-data.json 데이터 소스(스크립트 생성, 수동 수정 금지)
works-data.js 동일 데이터 인라인 버전(window.__WORKS_DATA__) — file:// 프로토콜에서 CORS 우회하여 더블 클릭 가능
works.js 렌더링 레이어: ROLES / cardHTML() / tileHTML() / bindFilter() / bindExpand()
extract-works.mjs 이전 단일 페이지에서 추출
patch-works.mjs 재번호 지정/모듈 보충/motif 할당(멱등성 필수!)
왜 JS 데이터를 인라인해야 하는가: fetch('./works-data.json')이 file:// 프로토콜에서 CORS에 차단됩니다. 사용자가 더블 클릭하면 빈 화면이 나타납니다.
멱등성 패치 스크립트: patch-works.mjs는 반복 실행 가능해야 합니다. EXTRA_TITLES를 사용하여 이미 패치된 항목을 필터링해야 합니다. 그렇지 않으면 두 번째 실행 시 "매핑 테이블에 없음" 오류가 발생합니다.
모듈 번호와 실제 디렉터리 정렬: 포트폴리오 번호를 카테고리가 아닌 로컬 디렉터리/GitHub 리포지토리의 실제 소유권에 따라 정렬합니다. "디렉터리가 있는 항목은 디렉터리 번호에 맞추고, 없는 항목은 빈자리를 채움" 방식으로 연속적이고 간격이 없도록 합니다. 번호를 변경할 때 사이트 전체 참조(진입 카드, 직무 페이지 배너, 소개 페이지 카테고리)를 동기화합니다. 진입 카드를 데이터에서 자동 생성하는 것이 가장 좋습니다. 수동 작성으로 인한 불일치를 방지합니다:
document.querySelectorAll('[data-owns]').forEach(function (el) {
var r = WORKS.ROLES[el.getAttribute('data-owns')];
if (r) el.textContent = '보유 ' + r.owns.length + '개 모듈(' + r.owns.join(' / ') + ')';
});
2. 스크린샷이 없을 때: 인라인 SVG 일러스트레이션
포트폴리오에는 대개 스크린샷이 없습니다. 카테고리별로 기하학적 모티프를 할당하고 인라인 SVG를 생성합니다(외부 이미지 없음, 첫 화면 이미지 요청 없음):
| 카테고리 | 모티프 | 요소 |
|---|---|---|
| 계획 | roadmap | 노드 + 점선 연결 + 마일스톤 |
| 워크플로우 | pipeline | 입력→처리→검토→출력 + 화살표 마커 |
| 에이전트 | neural | 계층형 노드 + 완전 연결 엣지 |
| 데이터 | metrics | 막대 + 꺾은선 추세 |
| 오픈소스 | terminal | 어두운 창 + 컬러 코드 줄 + 커서 |
| 산업 | grid | 청사진 그리드 |
각 SVG는 role="img"와 의미론적 aria-label을 포함해야 합니다.
문제 기록:
- SVG 내부의
<text>가 카드 그라데이션에 가려짐 → 태그를 SVG 밖으로 이동, HTML 오버레이.w-cover-tag사용(position:absolute) preserveAspectRatio는meet사용,slice사용 금지(잘림 방지)- 배열 인덱스 오류: 모티프 데이터가
[x,y,text,color]로 작성되었지만 렌더링이[x,y,width,color]로 해석되어l[2]를 가져옴 → 모든 사각형이 실패하고 커서만 표시됨. 모티프를 작성할 때 인덱스 의미가 일관되는지 먼저 확인하세요.
3. 3D 및 모션 효과(보기 좋지만 프레임 드롭 없어야 함)
배경: SpaceX 스타일 원근감 그리드
.bg-grid-floor {
position: absolute; left: -30%; right: -30%; top: 16%; height: 90%;
background-image:
repeating-linear-gradient(90deg, rgba(10,10,10,.16) 0 1px, transparent 1px 74px),
repeating-linear-gradient(180deg, rgba(10,10,10,.15) 0 1px, transparent 1px 40px);
transform: perspective(520px) rotateX(72deg) translateZ(0);
transform-origin: 50% 0%;
mask-image: linear-gradient(to bottom, transparent 0%, #000 11%, #000 52%, transparent 90%);
animation: floorDrift 30s linear infinite; /* background-position 변경, transform 사용 금지 */
}
그 위에 수평선 광선 + 세 개의 떠다니는 광점(각각 24/29/34초 호흡)을 겹칩니다.
성능 철칙(프레임 드롭의 분기점)
절대 filter: blur()와 backdrop-filter를 사용하지 마세요. 이 두 가지는 매 프레임마다 뒤 내용을 다시 래스터화/샘플링하여 스크롤 프레임 드롭의 주요 원인입니다:
- 부드러운 광점:
radial-gradient(circle, rgba(...).10, transparent 68%)사용 — 그라데이션 자체가 부드럽기 때문에 blur 불필요 - 모서리 유리 네비게이션: 거의 불투명한 배경
rgba(255,255,255,.96)사용 - 섹션 바닥: 반투명 흰색 배경 사용, backdrop-filter 사용 금지
애니메이션은 transform / opacity만 조작(합성기 처리, 레이아웃/페인트 트리거 없음). 스크롤 패럴랙스는 requestAnimationFrame 스로틀링 + translate3d 사용.
카드 3D 기울이기
pointermove는 CSS 변수를 작성하고 DOM을 건드리지 않음:
.tilt { transform: perspective(1000px) rotateX(var(--rx,0deg)) rotateY(var(--ry,0deg)); }
.tilt.is-tilting { transition: transform .06s linear; } /* 따라갈 때 transition 감소 */
.tilt .w-body { transform: translateZ(18px); }로 패럴랙스 부양 효과 생성; ::before 방사형 하이라이트가 --mx/--my를 따름.
hover 변위와 진입 애니메이션이 transform을抢夺함 → hover는 box-shadow/border-color만 변경, 변위는 모두 .tilt에 위임.
4. 진입 애니메이션: 절대 순수 CSS opacity:0 사용 금지
이것이 가장 큰 함정입니다. 순수 CSS로 시작 시 숨기고 JS로 .is-revealed를 추가하는 방식은, IntersectionObserver가 콜백하지 않으면(헤드리스 브라우저, JS 오류, 비정상 경로) 전체 페이지가 영구적으로 보이지 않게 됩니다.
올바른 방법: JS가 필요에 따라 숨김 상태 클래스를 추가하고, 클래스가 없는 요소는 기본적으로 보이게 합니다:
.w-card.reveal-armed { opacity: 0; transform: translateY(18px); }
.is-revealed { opacity: 1 !important; transform: none !important; }
list.forEach(function (el) {
var r = el.getBoundingClientRect();
var inView = r.top < innerHeight + 150 && r.bottom > -150;
if (!inView) el.classList.add('reveal-armed'); // 뷰포트 밖의 요소만 숨김
io.observe(el);
});
// 폴백: 3초 후에도 표시되지 않으면 강제 표시
setTimeout(function () { /* force reveal */ }, 3000);
뷰포트 내의 요소는 즉시 보이고, 뷰포트 밖의 요소만 무장됨 — 첫 화면이 애니메이션에 의해 지연되지 않고, 비정상 경로에서도 빈 화면이 발생하지 않습니다.
5. 스크립트 주입: <script> 열기/닫기 쌍 검증
여러 페이지에 코드를 일괄 주입할 때 <script>와 </script>의 개수가 동일한지 반드시 검증해야 합니다.
실제 사고: 주입 시 여는 태그 <script>를 하나 더 작성 → 이후의 인라인 스크립트 블록이 JS 파서에 의해 모두 소멸 → 네 개의 직무 페이지 렌더링 코드가 모두 작동 중단. 페이지는 완전히 정상으로 보였습니다(제목, 내비게이션, 직무 목록 모두 있음). 포트폴리오 영역만 비어 있어 육안으로 거의 발견할 수 없었습니다.
정규식 주입 시 주의: LastIndexOf('<script>')는 <script src=...>도 일치하여 정의가 태그 밖에 삽입되어 표시 텍스트가 됩니다. 명확한 앵커 문자열을 사용하세요.
6. 검증 방법(재사용 가능)
스크린샷 검증(중요: 동일 파일 이름은 캐시 사용)
vision_html_screenshot은 동일 파일 이름에 대해 캐시를 사용합니다. CSS를 변경한 후에는 반드시 Copy-Item을 사용하여 .verify/ 디렉터리에 임시 이름으로 복사한 다음 스크린샷을 찍고, 검증 후 샌드박스를 삭제하세요.
동기 프로브(load + setTimeout 사용 금지)
헤드리스 환경에서 window.load + setTimeout은 신뢰할 수 없습니다. 프로브를 렌더링 콜백 내부에 작성(동기 실행)하고 position:fixed;bottom:0인 <pre>에 출력하세요:
WORKS.loadData(function (data) {
root.innerHTML = WORKS.listByRole(data.items, 'fde');
var d = document.createElement('pre');
d.style.cssText='position:fixed;bottom:0;left:0;right:0;z-index:99999;background:#000;color:#0f0;font:11px monospace;padding:4px;margin:0';
d.textContent = 'CARDS=' + root.querySelectorAll('.w-card').length;
document.body.appendChild(d);
});
⚠️ 프로브에 top 위치 지정을 사용하지 마세요 — 카드 3D 기울기의 transform이 fixed 요소를 밀어낼 수 있습니다.
일괄 검증(페이지별 파일 읽기보다 효율적)
- 깨진 링크: 정규식으로
./xx/yy.md추출 →Test-Path로 각각 검증 - 번호 연속성:
Compare-Object로 예상 시퀀스 대비 실제 시퀀스 비교 - 디렉터리 정렬: PowerShell에서
'01'비교 시 int 1로 변환되므로[string]강제 형변환 필수 - 고가 기능 잔여:
[regex]::Matches($css,'filter:\s*blur|backdrop-filter'), 주석 구분에 주의
7. GitHub Pages에 배포
gh repo create <user>/<repo> --public --description "..."
git remote add origin https://github.com/<user>/<repo>.git
git push -u origin main
배포 전 필수 확인:
git ls-files로 종속 디렉터리가 실제로 추적되는지 확인(로컬에 파일이 있다고 git이 추적하는 것은 아님)- 원격에 이미 콘텐츠가 있다면 먼저
git ls-tree --name-only origin/main으로 비교하고, 덮어써서 손실되지 않도록 주의 - 로컬 상대 경로에 의존하는 형제 프로젝트 디렉터리: 함께 푸시하거나 GitHub 리포지토리 주소로 변경
- GitHub Pages: Settings → Pages → Source에서 브랜치 선택
- 하위 애플리케이션(예:
hitech-self-assessment/)이 있는 경우 메인 사이트 교체로 덮어쓰지 않도록 주의
성능 추정: 총 바이트 / 8 / 1.6e6 + 4 * 0.4(초)
8. 전달 전 자체 점검(web-delivery-checklist 정렬)
- 네/다섯 페이지 모두 스크린샷 검증 완료(첫 화면만 검증한 것이 아님)
- 깨진 링크 0개, 누락된 alt 0개
-
<script>열기/닫기 쌍 정상, CSS 중괄호 균형 -
filter: blur/backdrop-filter실제 적용 수 = 0 - 3G 추정 < 3초
- 기본 색상 ≤3개, 글꼴 ≤2개, 외부 CDN 리소스 0개
- 모바일(390px) 햄버거 메뉴 펼침/접힘 가능,
aria-expanded동기화 -
prefers-reduced-motion에서 모든 애니메이션 비활성화