diff --git a/plugins/webcraft/dist/claude/plugins/webcraft/.claude-plugin/plugin.json b/plugins/webcraft/dist/claude/plugins/webcraft/.claude-plugin/plugin.json index 32e6675..5cbf74c 100644 --- a/plugins/webcraft/dist/claude/plugins/webcraft/.claude-plugin/plugin.json +++ b/plugins/webcraft/dist/claude/plugins/webcraft/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "webcraft", - "version": "0.7.1", + "version": "0.7.2", "description": "웹 플랫폼 개발 지침(프론트·디자인 시스템·백엔드·API·DB·아키텍처)과 프로젝트 연결", "author": { "name": "dev-goraebap" diff --git a/plugins/webcraft/dist/claude/plugins/webcraft/package.json b/plugins/webcraft/dist/claude/plugins/webcraft/package.json index e747e08..323d44d 100644 --- a/plugins/webcraft/dist/claude/plugins/webcraft/package.json +++ b/plugins/webcraft/dist/claude/plugins/webcraft/package.json @@ -1,6 +1,6 @@ { "name": "webcraft", - "version": "0.7.1", + "version": "0.7.2", "private": true, "type": "module" } diff --git a/plugins/webcraft/dist/claude/plugins/webcraft/skills/design-system/references/data-states.md b/plugins/webcraft/dist/claude/plugins/webcraft/skills/design-system/references/data-states.md index f82ea60..8bbfaff 100644 --- a/plugins/webcraft/dist/claude/plugins/webcraft/skills/design-system/references/data-states.md +++ b/plugins/webcraft/dist/claude/plugins/webcraft/skills/design-system/references/data-states.md @@ -14,6 +14,20 @@ 이미 보이는 내용이 있는 재조회 실패에는 전체 상태 화면을 덮지 않습니다. 내용을 남기고 배너나 그 구역의 한 줄로 알립니다. 일부 출처만 실패하면 실패한 것을 빼고 보여 준다고 알립니다. +## 화면을 바꾸는 동안 + +캐시가 없는 화면의 데이터와 코드가 준비될 때까지 이전 화면을 유지합니다. 지연이 짧으면 표시를 생략하고, 길어지면 앱 셸을 그대로 둔 채 콘텐츠 영역에 불투명한 막을 표시합니다. 막과 새 본문을 함께 흐리게 만들지 않습니다. 새 화면의 그리기와 긴 작업이 끝났는지 확인한 뒤 잠깐 안정화 시간을 두고 막만 걷습니다. 숨은 탭이나 프레임이 멈춘 경우에도 끝나는 시간 제한과 취소 처리를 둡니다. + +기본 시작값은 표시 지연 100ms, 한 번 표시한 뒤 최소 300ms, 그리기 확인 뒤 안정화 200ms, 막 해제 700ms입니다. 제품에 확정된 값이 있으면 그 값을 따릅니다. 최소 표시 시간만 지났다고 새 본문의 레이아웃이 안정되기 전에 막을 걷지 않습니다. + +캐시가 있는 화면의 전환 효과는 콘텐츠에만 적용합니다. View Transition을 쓰면 문서 전체의 기본 스냅샷을 제외하고 이름을 붙인 콘텐츠만 캡처합니다. 로딩 막이 보이거나 걷히는 동안에는 겹치지 않습니다. 동작 줄이기 설정을 따르고, 빠른 연속 이동에서 진행 중인 전환을 겹쳐 실행하지 않습니다. + +## 대화상자의 데이터 준비 + +준비 중에는 이름 있는 원형 Spinner를 보이고, 필요한 데이터가 준비되면 내용을 한 번에 표시합니다. 바깥 창과 준비 영역의 최소 크기를 유지해 작은 창에서 갑자기 커지지 않게 합니다. 준비 확인 Promise가 새로 생겼다는 이유만으로 캐시에 있는 내용을 가리지 않습니다. 필요한 조회가 실제 캐시에 남아 있는지 확인하며, 한 번 표시한 로딩에는 최소 표시 시간을 적용합니다. 실패는 무한 로딩 대신 실패 상태와 다시 시도로 끝납니다. + +확인할 때는 첫 방문, 캐시가 있는 재방문, 느린 코드·데이터, 중첩 경로, 빠른 연속 이동과 동작 줄이기를 나눕니다. 애니메이션 호출 여부뿐 아니라 앱 셸·본문이 숨는 프레임과 로딩 막의 범위도 확인합니다. + ## 상태 화면 [PageState](https://github.com/dev-goraebap/dev.goraebap/blob/main/site/src/shared/ui/composites/state/PageState.tsx)는 펜 선 그림·제목·필요할 때의 설명·행동입니다. 그림은 테마 색으로 칠해 라이트·다크에서 같은 파일을 씁니다. 페이지에서는 문구를 창 높이의 32%에 고정해 필터 줄·탭이 다른 페이지 사이를 오가도 흔들리지 않습니다. 대화상자·패널·작은 구역은 위쪽에 두는 작은 형태(`isCompact`)를 씁니다. 그림은 장식이라 접근성 트리에서 숨기고, 이미 본 그림은 다시 흐리게 시작하지 않습니다. diff --git a/plugins/webcraft/dist/claude/plugins/webcraft/skills/design-system/references/dialog.md b/plugins/webcraft/dist/claude/plugins/webcraft/skills/design-system/references/dialog.md index 000706d..9c65581 100644 --- a/plugins/webcraft/dist/claude/plugins/webcraft/skills/design-system/references/dialog.md +++ b/plugins/webcraft/dist/claude/plugins/webcraft/skills/design-system/references/dialog.md @@ -13,6 +13,10 @@ 위치를 지정한 Dialog에 공통 margin을 더하면 top과 겹쳐 아래로 밀립니다. 위치와 남은 높이를 함께 정하고 짧은 화면에서도 닫기와 마지막 내용에 닿는지 확인합니다. +너비와 최소 크기는 쓰임별로 정하고 준비·실패·완료 상태에서 같은 기준을 씁니다. 비어 있는 로딩 틀의 크기를 내용의 고유 너비로 삼아 아주 작게 열지 않습니다. 긴 상세의 최소 높이는 작은 화면의 최대 높이를 넘지 않게 제한합니다. 짧은 확인 창과 본문이 있는 상세를 이유 없이 같은 크기로 강제하지 않습니다. + +제품이 대화상자 내부 이동을 지원하면 모달·배경·열기 애니메이션을 유지하는 바깥 틀 하나를 둡니다. 대상 주소가 바뀌었다고 바깥 Dialog의 key나 open 상태를 바꾸지 않습니다. 원형 로딩 표시와 완성된 내용을 이 틀 안에서 교체하고, 별도의 확인 창만 새 모달로 엽니다. 캐시가 있는 재열기, 데이터가 없는 첫 열기, 여러 번 내부 이동한 뒤 닫기·배경·Esc를 각각 확인합니다. + ## 쓰임에 따른 세 가지 | 쓰임 | 부품 | 동작 | diff --git a/plugins/webcraft/dist/claude/plugins/webcraft/skills/design-system/references/interaction-patterns.md b/plugins/webcraft/dist/claude/plugins/webcraft/skills/design-system/references/interaction-patterns.md index 58b7b43..9dd0162 100644 --- a/plugins/webcraft/dist/claude/plugins/webcraft/skills/design-system/references/interaction-patterns.md +++ b/plugins/webcraft/dist/claude/plugins/webcraft/skills/design-system/references/interaction-patterns.md @@ -18,6 +18,8 @@ 행·카드 전체를 누르면 그 대상을 엽니다. 행 안의 링크·사람·별·메뉴는 행을 열지 않고 자기 동작을 합니다. 화면 이동은 링크(`href`)로 해 새 탭·주소 복사가 되게 합니다. 펼치기·별처럼 행 안의 버튼은 행 링크로 번지지 않게 합니다. +페이지를 대화상자에서도 쓰도록 바꿀 때 행 전체의 링크를 제목 텍스트 링크로 줄이지 않습니다. 행의 빈 곳과 설명·아이콘을 눌러 같은 대상으로 열리는지 확인하고, 행 안의 별도 동작은 구분합니다. + ## 선택 칸 작은 고정 선택지는 Selector, 큰 목록에서 찾는 입력은 검색이 있는 고르기(처음 8개·최대 50개)입니다. 검색을 열면 입력에 초점을 두고, 고른 뒤나 Esc 뒤에는 열었던 칸으로 초점을 돌립니다. 검색 중에 보이지 않는 선택 항목도 지킵니다. 대기·결과 없음·추가 결과 안내를 구분합니다. diff --git a/plugins/webcraft/dist/claude/plugins/webcraft/skills/design-system/references/route-masking.md b/plugins/webcraft/dist/claude/plugins/webcraft/skills/design-system/references/route-masking.md index 7250e1c..9a45f07 100644 --- a/plugins/webcraft/dist/claude/plugins/webcraft/skills/design-system/references/route-masking.md +++ b/plugins/webcraft/dist/claude/plugins/webcraft/skills/design-system/references/route-masking.md @@ -8,7 +8,8 @@ | 직접 링크·새로고침(`unmaskOnReload`) | 대상 페이지 | | 휴대폰에서 열기, 휴대폰 폭에서 남은 `?panel` | 대상 페이지 | | 크게 보기 | 대상 페이지로 바꿔 갑니다(replace). 초안이 남습니다 | -| 닫기·Esc | 이 세션에서 열었으면 히스토리 뒤로(목록의 조건·스크롤 그대로), 아니면 패널만 뺀 목록 | +| 내부에서 허용한 다른 대상으로 이동 | 같은 마스킹 이력을 replace하고 바깥 대화상자는 유지 | +| 닫기·배경·Esc | 내부 이동 횟수와 관계없이 창 전체를 닫아 처음 연 목록의 조건·스크롤로 복귀. 처음 연 이력이 없으면 패널만 뺀 목록 | | 저장 성공 | 결과 페이지로 바꿔 갑니다(replace). 뒤로 가면 목록 | 상세처럼 읽는 대상은 자기 페이지로 열고, 목록을 보며 훑는 것은 [미리보기 패널](peek.md)로 엽니다. 목록 위 대화상자는 작성·수정, 프로필 같은 짧은 보기, 전역 검색에 씁니다. 자기 페이지가 없는 짧은 양식(이름 바꾸기, 폴더 만들기)은 크게 보기 없이 대화상자로만 엽니다. diff --git a/plugins/webcraft/dist/claude/plugins/webcraft/skills/frontend/references/navigation.md b/plugins/webcraft/dist/claude/plugins/webcraft/skills/frontend/references/navigation.md index f34a8aa..548ff19 100644 --- a/plugins/webcraft/dist/claude/plugins/webcraft/skills/frontend/references/navigation.md +++ b/plugins/webcraft/dist/claude/plugins/webcraft/skills/frontend/references/navigation.md @@ -2,6 +2,10 @@ 첫 화면에 필요한 조회는 라우트 loader에서 준비하고 컴포넌트와 같은 queryOptions·캐시를 씁니다. 캐시가 있으면 바로 그리고, 없으면 다음 화면이 준비될 때까지 지금 화면을 유지하며 짧은 대기에는 진행 표시를 띄우지 않습니다. 메뉴·링크 진입, 뒤로 돌아오기, 검색 조건 변경을 구분해 로딩·스크롤·캐시 정책을 정합니다. 화면 표현은 디자인 시스템의 [데이터 상태](../../design-system/references/data-states.md)를 따릅니다. +지연 로딩하는 화면 코드는 데이터와 함께 전환 전에 준비합니다. 라우트 안에서 고르는 중첩 화면도 포함합니다. 바깥 라우트만 준비하고 안쪽 lazy 컴포넌트가 렌더 중 처음 로드되면 Suspense가 앱 셸까지 숨길 수 있습니다. 새 경로의 일치 항목이 그려지는 시점과 `onResolved` 시점을 구분하고, 새 화면에 이전 주소를 전달해 본문을 다시 마운트하지 않습니다. + +본문뿐 아니라 첫 화면에 쓰는 목록·사람·선택지·연결 정보도 같은 조회 옵션으로 준비합니다. 하위 컴포넌트가 나중에 시작하는 조회와 URL 조건을 빠뜨리지 않습니다. 캐시를 채우는 loader의 라우터 캐시와 Query 캐시 수명을 따로 정해, loader가 생략된 뒤 빈 Query 캐시에서 화면이 다시 조회하지 않게 합니다. + - 검색 조건과 탭은 검증한 URL에서 읽습니다. 입력 중 검색은 replace, 사용자가 구분해서 돌아올 이동은 push를 기본으로 삼습니다. - 뒤로 돌아온 목록은 가능한 한 읽던 위치와 로드한 행을 유지합니다. 새로 진입할 때의 최신성 정책과 섞지 않습니다. - 전체 페이지와 상세 패널이 같은 대상을 열면 조회와 실제 화면 부품을 공유합니다. 데모·플레이그라운드도 제품 부품에 가짜 데이터만 연결합니다. @@ -9,6 +13,8 @@ 목록 위에서 상세를 열더라도 대상에는 직접 접근할 수 있는 주소를 둡니다. 패널·모달 표현을 사용한다면 직접 URL 진입과 새로고침에서는 독립 화면으로 열리는지도 확인합니다. 닫기는 이전 화면 맥락을 복구하고, 이전 기록이 없으면 해당 목록으로 가는 명시적인 대체 경로를 제공합니다. 브라우저 뒤로가기만 무조건 호출하지 않습니다. +대화상자 안에서 허용한 대상으로 이동할 때는 바깥 모달을 유지하고 내용만 교체합니다. 내부 이동은 같은 대화상자 이력 항목을 replace하며, 닫기·배경·Esc는 내부의 직전 상세가 아니라 대화상자 전체를 닫습니다. 처음 연 위치와 조건은 별도로 보존하고, 브라우저 뒤로가기와 닫기를 같은 의미로 다루지 않습니다. 별도의 확인 창은 이 내부 이동과 구분합니다. + 상세·만들기의 모바일 전체 화면 여부와 탐색 바 배치는 디자인 시스템의 레이아웃 기준을 따릅니다. 라우트에서 전달하는 화면 종류와 셸이 판단하는 화면 종류가 어긋나지 않게 합니다. 목록 위 대화상자(마스킹)는 자기 주소가 있는 작성·수정 양식에 쓰고, 읽는 대상의 상세는 자기 페이지로 엽니다. TanStack Router의 마스킹을 채택하면 대상의 직접 진입 경로와 목록 위의 패널 상태를 함께 정의합니다. `unmaskOnReload`를 설정해 새로고침 정책을 명시하고, 닫기에는 라우터의 히스토리를 사용합니다. 메모리 히스토리로 실행하는 견본에서도 같은 훅이 동작해야 합니다. 초안은 패널·페이지의 표현 전환보다 오래 사는 경계(라우터 위의 초안 저장소)에 두어 크게 보기·분기점 전환에서도 제목·본문·속성을 함께 보존하고, 저장 성공·버리기에서만 지웁니다. Astryx 부품의 링크는 루트의 LinkProvider로 라우터 이동에 연결합니다. [공통 구현과 실행 견본](../../design-system/references/pattern-catalog.md)에서 경로별 계약을 확인합니다. diff --git a/plugins/webcraft/dist/codex/plugins/webcraft/.codex-plugin/plugin.json b/plugins/webcraft/dist/codex/plugins/webcraft/.codex-plugin/plugin.json index a309c20..9cb8612 100644 --- a/plugins/webcraft/dist/codex/plugins/webcraft/.codex-plugin/plugin.json +++ b/plugins/webcraft/dist/codex/plugins/webcraft/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "webcraft", - "version": "0.7.1", + "version": "0.7.2", "description": "웹 플랫폼 개발 지침(프론트·디자인 시스템·백엔드·API·DB·아키텍처)과 프로젝트 연결", "author": { "name": "dev-goraebap" diff --git a/plugins/webcraft/dist/codex/plugins/webcraft/package.json b/plugins/webcraft/dist/codex/plugins/webcraft/package.json index e747e08..323d44d 100644 --- a/plugins/webcraft/dist/codex/plugins/webcraft/package.json +++ b/plugins/webcraft/dist/codex/plugins/webcraft/package.json @@ -1,6 +1,6 @@ { "name": "webcraft", - "version": "0.7.1", + "version": "0.7.2", "private": true, "type": "module" } diff --git a/plugins/webcraft/dist/codex/plugins/webcraft/skills/design-system/references/data-states.md b/plugins/webcraft/dist/codex/plugins/webcraft/skills/design-system/references/data-states.md index f82ea60..8bbfaff 100644 --- a/plugins/webcraft/dist/codex/plugins/webcraft/skills/design-system/references/data-states.md +++ b/plugins/webcraft/dist/codex/plugins/webcraft/skills/design-system/references/data-states.md @@ -14,6 +14,20 @@ 이미 보이는 내용이 있는 재조회 실패에는 전체 상태 화면을 덮지 않습니다. 내용을 남기고 배너나 그 구역의 한 줄로 알립니다. 일부 출처만 실패하면 실패한 것을 빼고 보여 준다고 알립니다. +## 화면을 바꾸는 동안 + +캐시가 없는 화면의 데이터와 코드가 준비될 때까지 이전 화면을 유지합니다. 지연이 짧으면 표시를 생략하고, 길어지면 앱 셸을 그대로 둔 채 콘텐츠 영역에 불투명한 막을 표시합니다. 막과 새 본문을 함께 흐리게 만들지 않습니다. 새 화면의 그리기와 긴 작업이 끝났는지 확인한 뒤 잠깐 안정화 시간을 두고 막만 걷습니다. 숨은 탭이나 프레임이 멈춘 경우에도 끝나는 시간 제한과 취소 처리를 둡니다. + +기본 시작값은 표시 지연 100ms, 한 번 표시한 뒤 최소 300ms, 그리기 확인 뒤 안정화 200ms, 막 해제 700ms입니다. 제품에 확정된 값이 있으면 그 값을 따릅니다. 최소 표시 시간만 지났다고 새 본문의 레이아웃이 안정되기 전에 막을 걷지 않습니다. + +캐시가 있는 화면의 전환 효과는 콘텐츠에만 적용합니다. View Transition을 쓰면 문서 전체의 기본 스냅샷을 제외하고 이름을 붙인 콘텐츠만 캡처합니다. 로딩 막이 보이거나 걷히는 동안에는 겹치지 않습니다. 동작 줄이기 설정을 따르고, 빠른 연속 이동에서 진행 중인 전환을 겹쳐 실행하지 않습니다. + +## 대화상자의 데이터 준비 + +준비 중에는 이름 있는 원형 Spinner를 보이고, 필요한 데이터가 준비되면 내용을 한 번에 표시합니다. 바깥 창과 준비 영역의 최소 크기를 유지해 작은 창에서 갑자기 커지지 않게 합니다. 준비 확인 Promise가 새로 생겼다는 이유만으로 캐시에 있는 내용을 가리지 않습니다. 필요한 조회가 실제 캐시에 남아 있는지 확인하며, 한 번 표시한 로딩에는 최소 표시 시간을 적용합니다. 실패는 무한 로딩 대신 실패 상태와 다시 시도로 끝납니다. + +확인할 때는 첫 방문, 캐시가 있는 재방문, 느린 코드·데이터, 중첩 경로, 빠른 연속 이동과 동작 줄이기를 나눕니다. 애니메이션 호출 여부뿐 아니라 앱 셸·본문이 숨는 프레임과 로딩 막의 범위도 확인합니다. + ## 상태 화면 [PageState](https://github.com/dev-goraebap/dev.goraebap/blob/main/site/src/shared/ui/composites/state/PageState.tsx)는 펜 선 그림·제목·필요할 때의 설명·행동입니다. 그림은 테마 색으로 칠해 라이트·다크에서 같은 파일을 씁니다. 페이지에서는 문구를 창 높이의 32%에 고정해 필터 줄·탭이 다른 페이지 사이를 오가도 흔들리지 않습니다. 대화상자·패널·작은 구역은 위쪽에 두는 작은 형태(`isCompact`)를 씁니다. 그림은 장식이라 접근성 트리에서 숨기고, 이미 본 그림은 다시 흐리게 시작하지 않습니다. diff --git a/plugins/webcraft/dist/codex/plugins/webcraft/skills/design-system/references/dialog.md b/plugins/webcraft/dist/codex/plugins/webcraft/skills/design-system/references/dialog.md index 000706d..9c65581 100644 --- a/plugins/webcraft/dist/codex/plugins/webcraft/skills/design-system/references/dialog.md +++ b/plugins/webcraft/dist/codex/plugins/webcraft/skills/design-system/references/dialog.md @@ -13,6 +13,10 @@ 위치를 지정한 Dialog에 공통 margin을 더하면 top과 겹쳐 아래로 밀립니다. 위치와 남은 높이를 함께 정하고 짧은 화면에서도 닫기와 마지막 내용에 닿는지 확인합니다. +너비와 최소 크기는 쓰임별로 정하고 준비·실패·완료 상태에서 같은 기준을 씁니다. 비어 있는 로딩 틀의 크기를 내용의 고유 너비로 삼아 아주 작게 열지 않습니다. 긴 상세의 최소 높이는 작은 화면의 최대 높이를 넘지 않게 제한합니다. 짧은 확인 창과 본문이 있는 상세를 이유 없이 같은 크기로 강제하지 않습니다. + +제품이 대화상자 내부 이동을 지원하면 모달·배경·열기 애니메이션을 유지하는 바깥 틀 하나를 둡니다. 대상 주소가 바뀌었다고 바깥 Dialog의 key나 open 상태를 바꾸지 않습니다. 원형 로딩 표시와 완성된 내용을 이 틀 안에서 교체하고, 별도의 확인 창만 새 모달로 엽니다. 캐시가 있는 재열기, 데이터가 없는 첫 열기, 여러 번 내부 이동한 뒤 닫기·배경·Esc를 각각 확인합니다. + ## 쓰임에 따른 세 가지 | 쓰임 | 부품 | 동작 | diff --git a/plugins/webcraft/dist/codex/plugins/webcraft/skills/design-system/references/interaction-patterns.md b/plugins/webcraft/dist/codex/plugins/webcraft/skills/design-system/references/interaction-patterns.md index 58b7b43..9dd0162 100644 --- a/plugins/webcraft/dist/codex/plugins/webcraft/skills/design-system/references/interaction-patterns.md +++ b/plugins/webcraft/dist/codex/plugins/webcraft/skills/design-system/references/interaction-patterns.md @@ -18,6 +18,8 @@ 행·카드 전체를 누르면 그 대상을 엽니다. 행 안의 링크·사람·별·메뉴는 행을 열지 않고 자기 동작을 합니다. 화면 이동은 링크(`href`)로 해 새 탭·주소 복사가 되게 합니다. 펼치기·별처럼 행 안의 버튼은 행 링크로 번지지 않게 합니다. +페이지를 대화상자에서도 쓰도록 바꿀 때 행 전체의 링크를 제목 텍스트 링크로 줄이지 않습니다. 행의 빈 곳과 설명·아이콘을 눌러 같은 대상으로 열리는지 확인하고, 행 안의 별도 동작은 구분합니다. + ## 선택 칸 작은 고정 선택지는 Selector, 큰 목록에서 찾는 입력은 검색이 있는 고르기(처음 8개·최대 50개)입니다. 검색을 열면 입력에 초점을 두고, 고른 뒤나 Esc 뒤에는 열었던 칸으로 초점을 돌립니다. 검색 중에 보이지 않는 선택 항목도 지킵니다. 대기·결과 없음·추가 결과 안내를 구분합니다. diff --git a/plugins/webcraft/dist/codex/plugins/webcraft/skills/design-system/references/route-masking.md b/plugins/webcraft/dist/codex/plugins/webcraft/skills/design-system/references/route-masking.md index 7250e1c..9a45f07 100644 --- a/plugins/webcraft/dist/codex/plugins/webcraft/skills/design-system/references/route-masking.md +++ b/plugins/webcraft/dist/codex/plugins/webcraft/skills/design-system/references/route-masking.md @@ -8,7 +8,8 @@ | 직접 링크·새로고침(`unmaskOnReload`) | 대상 페이지 | | 휴대폰에서 열기, 휴대폰 폭에서 남은 `?panel` | 대상 페이지 | | 크게 보기 | 대상 페이지로 바꿔 갑니다(replace). 초안이 남습니다 | -| 닫기·Esc | 이 세션에서 열었으면 히스토리 뒤로(목록의 조건·스크롤 그대로), 아니면 패널만 뺀 목록 | +| 내부에서 허용한 다른 대상으로 이동 | 같은 마스킹 이력을 replace하고 바깥 대화상자는 유지 | +| 닫기·배경·Esc | 내부 이동 횟수와 관계없이 창 전체를 닫아 처음 연 목록의 조건·스크롤로 복귀. 처음 연 이력이 없으면 패널만 뺀 목록 | | 저장 성공 | 결과 페이지로 바꿔 갑니다(replace). 뒤로 가면 목록 | 상세처럼 읽는 대상은 자기 페이지로 열고, 목록을 보며 훑는 것은 [미리보기 패널](peek.md)로 엽니다. 목록 위 대화상자는 작성·수정, 프로필 같은 짧은 보기, 전역 검색에 씁니다. 자기 페이지가 없는 짧은 양식(이름 바꾸기, 폴더 만들기)은 크게 보기 없이 대화상자로만 엽니다. diff --git a/plugins/webcraft/dist/codex/plugins/webcraft/skills/frontend/references/navigation.md b/plugins/webcraft/dist/codex/plugins/webcraft/skills/frontend/references/navigation.md index f34a8aa..548ff19 100644 --- a/plugins/webcraft/dist/codex/plugins/webcraft/skills/frontend/references/navigation.md +++ b/plugins/webcraft/dist/codex/plugins/webcraft/skills/frontend/references/navigation.md @@ -2,6 +2,10 @@ 첫 화면에 필요한 조회는 라우트 loader에서 준비하고 컴포넌트와 같은 queryOptions·캐시를 씁니다. 캐시가 있으면 바로 그리고, 없으면 다음 화면이 준비될 때까지 지금 화면을 유지하며 짧은 대기에는 진행 표시를 띄우지 않습니다. 메뉴·링크 진입, 뒤로 돌아오기, 검색 조건 변경을 구분해 로딩·스크롤·캐시 정책을 정합니다. 화면 표현은 디자인 시스템의 [데이터 상태](../../design-system/references/data-states.md)를 따릅니다. +지연 로딩하는 화면 코드는 데이터와 함께 전환 전에 준비합니다. 라우트 안에서 고르는 중첩 화면도 포함합니다. 바깥 라우트만 준비하고 안쪽 lazy 컴포넌트가 렌더 중 처음 로드되면 Suspense가 앱 셸까지 숨길 수 있습니다. 새 경로의 일치 항목이 그려지는 시점과 `onResolved` 시점을 구분하고, 새 화면에 이전 주소를 전달해 본문을 다시 마운트하지 않습니다. + +본문뿐 아니라 첫 화면에 쓰는 목록·사람·선택지·연결 정보도 같은 조회 옵션으로 준비합니다. 하위 컴포넌트가 나중에 시작하는 조회와 URL 조건을 빠뜨리지 않습니다. 캐시를 채우는 loader의 라우터 캐시와 Query 캐시 수명을 따로 정해, loader가 생략된 뒤 빈 Query 캐시에서 화면이 다시 조회하지 않게 합니다. + - 검색 조건과 탭은 검증한 URL에서 읽습니다. 입력 중 검색은 replace, 사용자가 구분해서 돌아올 이동은 push를 기본으로 삼습니다. - 뒤로 돌아온 목록은 가능한 한 읽던 위치와 로드한 행을 유지합니다. 새로 진입할 때의 최신성 정책과 섞지 않습니다. - 전체 페이지와 상세 패널이 같은 대상을 열면 조회와 실제 화면 부품을 공유합니다. 데모·플레이그라운드도 제품 부품에 가짜 데이터만 연결합니다. @@ -9,6 +13,8 @@ 목록 위에서 상세를 열더라도 대상에는 직접 접근할 수 있는 주소를 둡니다. 패널·모달 표현을 사용한다면 직접 URL 진입과 새로고침에서는 독립 화면으로 열리는지도 확인합니다. 닫기는 이전 화면 맥락을 복구하고, 이전 기록이 없으면 해당 목록으로 가는 명시적인 대체 경로를 제공합니다. 브라우저 뒤로가기만 무조건 호출하지 않습니다. +대화상자 안에서 허용한 대상으로 이동할 때는 바깥 모달을 유지하고 내용만 교체합니다. 내부 이동은 같은 대화상자 이력 항목을 replace하며, 닫기·배경·Esc는 내부의 직전 상세가 아니라 대화상자 전체를 닫습니다. 처음 연 위치와 조건은 별도로 보존하고, 브라우저 뒤로가기와 닫기를 같은 의미로 다루지 않습니다. 별도의 확인 창은 이 내부 이동과 구분합니다. + 상세·만들기의 모바일 전체 화면 여부와 탐색 바 배치는 디자인 시스템의 레이아웃 기준을 따릅니다. 라우트에서 전달하는 화면 종류와 셸이 판단하는 화면 종류가 어긋나지 않게 합니다. 목록 위 대화상자(마스킹)는 자기 주소가 있는 작성·수정 양식에 쓰고, 읽는 대상의 상세는 자기 페이지로 엽니다. TanStack Router의 마스킹을 채택하면 대상의 직접 진입 경로와 목록 위의 패널 상태를 함께 정의합니다. `unmaskOnReload`를 설정해 새로고침 정책을 명시하고, 닫기에는 라우터의 히스토리를 사용합니다. 메모리 히스토리로 실행하는 견본에서도 같은 훅이 동작해야 합니다. 초안은 패널·페이지의 표현 전환보다 오래 사는 경계(라우터 위의 초안 저장소)에 두어 크게 보기·분기점 전환에서도 제목·본문·속성을 함께 보존하고, 저장 성공·버리기에서만 지웁니다. Astryx 부품의 링크는 루트의 LinkProvider로 라우터 이동에 연결합니다. [공통 구현과 실행 견본](../../design-system/references/pattern-catalog.md)에서 경로별 계약을 확인합니다. diff --git a/plugins/webcraft/package.json b/plugins/webcraft/package.json index f69dacb..7a6d541 100644 --- a/plugins/webcraft/package.json +++ b/plugins/webcraft/package.json @@ -1,6 +1,6 @@ { "name": "webcraft", - "version": "0.7.1", + "version": "0.7.2", "private": true, "type": "module", "engines": { diff --git a/plugins/webcraft/skills/design-system/references/data-states.md b/plugins/webcraft/skills/design-system/references/data-states.md index f82ea60..8bbfaff 100644 --- a/plugins/webcraft/skills/design-system/references/data-states.md +++ b/plugins/webcraft/skills/design-system/references/data-states.md @@ -14,6 +14,20 @@ 이미 보이는 내용이 있는 재조회 실패에는 전체 상태 화면을 덮지 않습니다. 내용을 남기고 배너나 그 구역의 한 줄로 알립니다. 일부 출처만 실패하면 실패한 것을 빼고 보여 준다고 알립니다. +## 화면을 바꾸는 동안 + +캐시가 없는 화면의 데이터와 코드가 준비될 때까지 이전 화면을 유지합니다. 지연이 짧으면 표시를 생략하고, 길어지면 앱 셸을 그대로 둔 채 콘텐츠 영역에 불투명한 막을 표시합니다. 막과 새 본문을 함께 흐리게 만들지 않습니다. 새 화면의 그리기와 긴 작업이 끝났는지 확인한 뒤 잠깐 안정화 시간을 두고 막만 걷습니다. 숨은 탭이나 프레임이 멈춘 경우에도 끝나는 시간 제한과 취소 처리를 둡니다. + +기본 시작값은 표시 지연 100ms, 한 번 표시한 뒤 최소 300ms, 그리기 확인 뒤 안정화 200ms, 막 해제 700ms입니다. 제품에 확정된 값이 있으면 그 값을 따릅니다. 최소 표시 시간만 지났다고 새 본문의 레이아웃이 안정되기 전에 막을 걷지 않습니다. + +캐시가 있는 화면의 전환 효과는 콘텐츠에만 적용합니다. View Transition을 쓰면 문서 전체의 기본 스냅샷을 제외하고 이름을 붙인 콘텐츠만 캡처합니다. 로딩 막이 보이거나 걷히는 동안에는 겹치지 않습니다. 동작 줄이기 설정을 따르고, 빠른 연속 이동에서 진행 중인 전환을 겹쳐 실행하지 않습니다. + +## 대화상자의 데이터 준비 + +준비 중에는 이름 있는 원형 Spinner를 보이고, 필요한 데이터가 준비되면 내용을 한 번에 표시합니다. 바깥 창과 준비 영역의 최소 크기를 유지해 작은 창에서 갑자기 커지지 않게 합니다. 준비 확인 Promise가 새로 생겼다는 이유만으로 캐시에 있는 내용을 가리지 않습니다. 필요한 조회가 실제 캐시에 남아 있는지 확인하며, 한 번 표시한 로딩에는 최소 표시 시간을 적용합니다. 실패는 무한 로딩 대신 실패 상태와 다시 시도로 끝납니다. + +확인할 때는 첫 방문, 캐시가 있는 재방문, 느린 코드·데이터, 중첩 경로, 빠른 연속 이동과 동작 줄이기를 나눕니다. 애니메이션 호출 여부뿐 아니라 앱 셸·본문이 숨는 프레임과 로딩 막의 범위도 확인합니다. + ## 상태 화면 [PageState](https://github.com/dev-goraebap/dev.goraebap/blob/main/site/src/shared/ui/composites/state/PageState.tsx)는 펜 선 그림·제목·필요할 때의 설명·행동입니다. 그림은 테마 색으로 칠해 라이트·다크에서 같은 파일을 씁니다. 페이지에서는 문구를 창 높이의 32%에 고정해 필터 줄·탭이 다른 페이지 사이를 오가도 흔들리지 않습니다. 대화상자·패널·작은 구역은 위쪽에 두는 작은 형태(`isCompact`)를 씁니다. 그림은 장식이라 접근성 트리에서 숨기고, 이미 본 그림은 다시 흐리게 시작하지 않습니다. diff --git a/plugins/webcraft/skills/design-system/references/dialog.md b/plugins/webcraft/skills/design-system/references/dialog.md index 000706d..9c65581 100644 --- a/plugins/webcraft/skills/design-system/references/dialog.md +++ b/plugins/webcraft/skills/design-system/references/dialog.md @@ -13,6 +13,10 @@ 위치를 지정한 Dialog에 공통 margin을 더하면 top과 겹쳐 아래로 밀립니다. 위치와 남은 높이를 함께 정하고 짧은 화면에서도 닫기와 마지막 내용에 닿는지 확인합니다. +너비와 최소 크기는 쓰임별로 정하고 준비·실패·완료 상태에서 같은 기준을 씁니다. 비어 있는 로딩 틀의 크기를 내용의 고유 너비로 삼아 아주 작게 열지 않습니다. 긴 상세의 최소 높이는 작은 화면의 최대 높이를 넘지 않게 제한합니다. 짧은 확인 창과 본문이 있는 상세를 이유 없이 같은 크기로 강제하지 않습니다. + +제품이 대화상자 내부 이동을 지원하면 모달·배경·열기 애니메이션을 유지하는 바깥 틀 하나를 둡니다. 대상 주소가 바뀌었다고 바깥 Dialog의 key나 open 상태를 바꾸지 않습니다. 원형 로딩 표시와 완성된 내용을 이 틀 안에서 교체하고, 별도의 확인 창만 새 모달로 엽니다. 캐시가 있는 재열기, 데이터가 없는 첫 열기, 여러 번 내부 이동한 뒤 닫기·배경·Esc를 각각 확인합니다. + ## 쓰임에 따른 세 가지 | 쓰임 | 부품 | 동작 | diff --git a/plugins/webcraft/skills/design-system/references/interaction-patterns.md b/plugins/webcraft/skills/design-system/references/interaction-patterns.md index 58b7b43..9dd0162 100644 --- a/plugins/webcraft/skills/design-system/references/interaction-patterns.md +++ b/plugins/webcraft/skills/design-system/references/interaction-patterns.md @@ -18,6 +18,8 @@ 행·카드 전체를 누르면 그 대상을 엽니다. 행 안의 링크·사람·별·메뉴는 행을 열지 않고 자기 동작을 합니다. 화면 이동은 링크(`href`)로 해 새 탭·주소 복사가 되게 합니다. 펼치기·별처럼 행 안의 버튼은 행 링크로 번지지 않게 합니다. +페이지를 대화상자에서도 쓰도록 바꿀 때 행 전체의 링크를 제목 텍스트 링크로 줄이지 않습니다. 행의 빈 곳과 설명·아이콘을 눌러 같은 대상으로 열리는지 확인하고, 행 안의 별도 동작은 구분합니다. + ## 선택 칸 작은 고정 선택지는 Selector, 큰 목록에서 찾는 입력은 검색이 있는 고르기(처음 8개·최대 50개)입니다. 검색을 열면 입력에 초점을 두고, 고른 뒤나 Esc 뒤에는 열었던 칸으로 초점을 돌립니다. 검색 중에 보이지 않는 선택 항목도 지킵니다. 대기·결과 없음·추가 결과 안내를 구분합니다. diff --git a/plugins/webcraft/skills/design-system/references/route-masking.md b/plugins/webcraft/skills/design-system/references/route-masking.md index 7250e1c..9a45f07 100644 --- a/plugins/webcraft/skills/design-system/references/route-masking.md +++ b/plugins/webcraft/skills/design-system/references/route-masking.md @@ -8,7 +8,8 @@ | 직접 링크·새로고침(`unmaskOnReload`) | 대상 페이지 | | 휴대폰에서 열기, 휴대폰 폭에서 남은 `?panel` | 대상 페이지 | | 크게 보기 | 대상 페이지로 바꿔 갑니다(replace). 초안이 남습니다 | -| 닫기·Esc | 이 세션에서 열었으면 히스토리 뒤로(목록의 조건·스크롤 그대로), 아니면 패널만 뺀 목록 | +| 내부에서 허용한 다른 대상으로 이동 | 같은 마스킹 이력을 replace하고 바깥 대화상자는 유지 | +| 닫기·배경·Esc | 내부 이동 횟수와 관계없이 창 전체를 닫아 처음 연 목록의 조건·스크롤로 복귀. 처음 연 이력이 없으면 패널만 뺀 목록 | | 저장 성공 | 결과 페이지로 바꿔 갑니다(replace). 뒤로 가면 목록 | 상세처럼 읽는 대상은 자기 페이지로 열고, 목록을 보며 훑는 것은 [미리보기 패널](peek.md)로 엽니다. 목록 위 대화상자는 작성·수정, 프로필 같은 짧은 보기, 전역 검색에 씁니다. 자기 페이지가 없는 짧은 양식(이름 바꾸기, 폴더 만들기)은 크게 보기 없이 대화상자로만 엽니다. diff --git a/plugins/webcraft/skills/frontend/references/navigation.md b/plugins/webcraft/skills/frontend/references/navigation.md index f34a8aa..548ff19 100644 --- a/plugins/webcraft/skills/frontend/references/navigation.md +++ b/plugins/webcraft/skills/frontend/references/navigation.md @@ -2,6 +2,10 @@ 첫 화면에 필요한 조회는 라우트 loader에서 준비하고 컴포넌트와 같은 queryOptions·캐시를 씁니다. 캐시가 있으면 바로 그리고, 없으면 다음 화면이 준비될 때까지 지금 화면을 유지하며 짧은 대기에는 진행 표시를 띄우지 않습니다. 메뉴·링크 진입, 뒤로 돌아오기, 검색 조건 변경을 구분해 로딩·스크롤·캐시 정책을 정합니다. 화면 표현은 디자인 시스템의 [데이터 상태](../../design-system/references/data-states.md)를 따릅니다. +지연 로딩하는 화면 코드는 데이터와 함께 전환 전에 준비합니다. 라우트 안에서 고르는 중첩 화면도 포함합니다. 바깥 라우트만 준비하고 안쪽 lazy 컴포넌트가 렌더 중 처음 로드되면 Suspense가 앱 셸까지 숨길 수 있습니다. 새 경로의 일치 항목이 그려지는 시점과 `onResolved` 시점을 구분하고, 새 화면에 이전 주소를 전달해 본문을 다시 마운트하지 않습니다. + +본문뿐 아니라 첫 화면에 쓰는 목록·사람·선택지·연결 정보도 같은 조회 옵션으로 준비합니다. 하위 컴포넌트가 나중에 시작하는 조회와 URL 조건을 빠뜨리지 않습니다. 캐시를 채우는 loader의 라우터 캐시와 Query 캐시 수명을 따로 정해, loader가 생략된 뒤 빈 Query 캐시에서 화면이 다시 조회하지 않게 합니다. + - 검색 조건과 탭은 검증한 URL에서 읽습니다. 입력 중 검색은 replace, 사용자가 구분해서 돌아올 이동은 push를 기본으로 삼습니다. - 뒤로 돌아온 목록은 가능한 한 읽던 위치와 로드한 행을 유지합니다. 새로 진입할 때의 최신성 정책과 섞지 않습니다. - 전체 페이지와 상세 패널이 같은 대상을 열면 조회와 실제 화면 부품을 공유합니다. 데모·플레이그라운드도 제품 부품에 가짜 데이터만 연결합니다. @@ -9,6 +13,8 @@ 목록 위에서 상세를 열더라도 대상에는 직접 접근할 수 있는 주소를 둡니다. 패널·모달 표현을 사용한다면 직접 URL 진입과 새로고침에서는 독립 화면으로 열리는지도 확인합니다. 닫기는 이전 화면 맥락을 복구하고, 이전 기록이 없으면 해당 목록으로 가는 명시적인 대체 경로를 제공합니다. 브라우저 뒤로가기만 무조건 호출하지 않습니다. +대화상자 안에서 허용한 대상으로 이동할 때는 바깥 모달을 유지하고 내용만 교체합니다. 내부 이동은 같은 대화상자 이력 항목을 replace하며, 닫기·배경·Esc는 내부의 직전 상세가 아니라 대화상자 전체를 닫습니다. 처음 연 위치와 조건은 별도로 보존하고, 브라우저 뒤로가기와 닫기를 같은 의미로 다루지 않습니다. 별도의 확인 창은 이 내부 이동과 구분합니다. + 상세·만들기의 모바일 전체 화면 여부와 탐색 바 배치는 디자인 시스템의 레이아웃 기준을 따릅니다. 라우트에서 전달하는 화면 종류와 셸이 판단하는 화면 종류가 어긋나지 않게 합니다. 목록 위 대화상자(마스킹)는 자기 주소가 있는 작성·수정 양식에 쓰고, 읽는 대상의 상세는 자기 페이지로 엽니다. TanStack Router의 마스킹을 채택하면 대상의 직접 진입 경로와 목록 위의 패널 상태를 함께 정의합니다. `unmaskOnReload`를 설정해 새로고침 정책을 명시하고, 닫기에는 라우터의 히스토리를 사용합니다. 메모리 히스토리로 실행하는 견본에서도 같은 훅이 동작해야 합니다. 초안은 패널·페이지의 표현 전환보다 오래 사는 경계(라우터 위의 초안 저장소)에 두어 크게 보기·분기점 전환에서도 제목·본문·속성을 함께 보존하고, 저장 성공·버리기에서만 지웁니다. Astryx 부품의 링크는 루트의 LinkProvider로 라우터 이동에 연결합니다. [공통 구현과 실행 견본](../../design-system/references/pattern-catalog.md)에서 경로별 계약을 확인합니다. diff --git a/site/samples/patterns/RoutedPanelHistory.test.tsx b/site/samples/patterns/RoutedPanelHistory.test.tsx new file mode 100644 index 0000000..45d4f92 --- /dev/null +++ b/site/samples/patterns/RoutedPanelHistory.test.tsx @@ -0,0 +1,41 @@ +import { render } from '@testing-library/react'; +import { createMemoryHistory, createRootRoute, createRoute, createRouter, Outlet, RouterProvider } from '@tanstack/react-router'; +import { page } from 'vitest/browser'; +import { test } from 'vitest'; +import { expect, userEvent, waitFor, within } from '../catalog/testing'; +import { useOpenPanel, useRoutedPanel } from '../../src/shared/ui/patterns/routing/routed-panel'; + +function Rows() { + const open = useOpenPanel(); + const panel = useRoutedPanel(); + return <> + + {panel.href ?
+ {panel.href} + + +
: null} + ; +} + +test('내부 이동은 이력을 쌓지 않고 닫기는 원래 목록으로 돌아간다', async () => { + await page.viewport(1440, 960); + const root = createRootRoute({ component: Outlet }); + const rows = createRoute({ getParentRoute: () => root, path: '/rows', component: Rows }); + const editor = createRoute({ getParentRoute: () => root, path: '/editor/$id', component: () => null }); + const history = createMemoryHistory({ initialEntries: ['/rows?filter=kept'] }); + const router = createRouter({ routeTree: root.addChildren([rows, editor]), history, unmaskOnReload: true }); + await router.load(); + const view = render(); + const screen = within(view.container); + await userEvent.click(await screen.findByRole('button', { name: '열기' })); + await screen.findByText('/editor/first'); + expect(history.length).toBe(2); + await userEvent.click(screen.getByRole('button', { name: '다음 내용' })); + await screen.findByText('/editor/second'); + expect(history.length).toBe(2); + await userEvent.click(screen.getByRole('button', { name: '닫기' })); + await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument()); + expect(router.state.location.pathname).toBe('/rows'); + expect((router.state.location.search as Record).filter).toBe('kept'); +}); diff --git a/site/src/shared/ui/patterns/routing/routed-panel.ts b/site/src/shared/ui/patterns/routing/routed-panel.ts index f1b34e3..07ca8af 100644 --- a/site/src/shared/ui/patterns/routing/routed-panel.ts +++ b/site/src/shared/ui/patterns/routing/routed-panel.ts @@ -3,6 +3,7 @@ import { useIsPhone } from '../../foundations/media'; /** 이 세션에서 목록 위에 연 패널인지 표시하는 히스토리 상태 키. */ const OPENED = 'openedPanel'; +const ROOT_ENTRY = 'root'; /** * 데스크톱에서는 지금 목록을 둔 채 ?panel=에 대상 주소를 넣고, 주소창에는 대상의 실제 주소를 마스크로 보인다. 휴대폰은 @@ -15,9 +16,11 @@ export function useOpenPanel() { return (href: string) => { if (isPhone) return void navigate({ href }); const target = new URL(href, 'https://panel.invalid'); + const replacing = typeof (location.search as Record).panel === 'string'; + const marker = replacing ? (location.state as unknown as Record | undefined)?.[OPENED] : ROOT_ENTRY; void navigate({ - to: location.pathname, search: { ...(location.search as Record), panel: href }, state: { [OPENED]: true } as never, - mask: { to: target.pathname, search: Object.fromEntries(target.searchParams), state: { [OPENED]: true } as never, unmaskOnReload: true }, + replace: replacing, to: location.pathname, search: { ...(location.search as Record), panel: href }, state: { [OPENED]: marker } as never, + mask: { to: target.pathname, search: Object.fromEntries(target.searchParams), state: { [OPENED]: marker } as never, unmaskOnReload: true }, }); }; } @@ -32,7 +35,7 @@ export function useRoutedPanel() { const router = useRouter(); const isPhone = useIsPhone(); const { panel, ...rest } = location.search as Record; - const openedHere = !!(location.state as unknown as Record | undefined)?.[OPENED]; + const openedHere = (location.state as unknown as Record | undefined)?.[OPENED] === ROOT_ENTRY; const href = typeof panel === 'string' && panel.startsWith('/') ? panel : undefined; return { /** 휴대폰 폭에서는 패널을 그리지 않고 페이지로 옮긴다. */