feat: give the Asset Library server search and cursor paging

`StudioAssetGateway.listAssets` has always accepted `q`/`cursor` and
returned `nextCursor`, and this screen's own heading promises search
("업로드한 Asset을 검색하고..."). The component asked for `{ limit: 50 }`
and dropped `nextCursor`, so past the 51st asset older assets were
unmanageable with nothing on screen admitting anything was left out.

Follows `document-list.tsx`'s established shape: a `searchDraft`/`q`
pair so only a submitted query is an effect dependency (exactly one
request per deliberate search, never a trailing one), and a
`nextCursor`-driven control that appends rather than replaces.

Two things the pattern did not already cover:

- `RequestOptions.signal` is advisory, so aborting is not enough to stop
  an abandoned response from painting over a newer one. An `active` flag
  that flips synchronously on dependency change closes that race.
- "더 보기" unmounts exactly when the last page arrives, which would
  strand focus on `<body>`. Focus moves to the first appended row
  unconditionally, falling back to the page heading -- the same anchor
  the delete path already uses.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-08-18 13:12:58 +09:00
co-authored by Claude Opus 5
parent 419d9d006d
commit a5825c18b5
3 changed files with 385 additions and 12 deletions
@@ -51,6 +51,7 @@
.studio-app .studio-primary-button,
.studio-app .studio-secondary-button,
.studio-app .studio-document-tools button,
.studio-app .studio-asset-tools button,
.studio-app .studio-empty-state a { display: inline-flex; min-height: 44px; align-items: center; justify-content: center; padding-inline: 16px; border: 1px solid var(--line-strong); border-radius: 5px; background: var(--paper); color: var(--ink); font-weight: 650; }
.studio-app .studio-primary-action,
.studio-app .studio-primary-button { margin-bottom: 42px; border-color: var(--signal); background: var(--signal); color: #fff; }
@@ -76,12 +77,18 @@
.studio-app .studio-screen-error { margin-top: 26px; padding: 16px; border-left: 3px solid #a13b31; background: #fff7f4; color: #75271f; }
.studio-app .studio-document-tools { display: grid; grid-template-columns: minmax(280px, 1fr) 180px 180px; gap: 16px; padding-block: 28px; border-bottom: 1px solid var(--line-strong); }
.studio-app .studio-asset-tools { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; padding-block: 28px; border-bottom: 1px solid var(--line-strong); }
.studio-app .studio-document-tools form,
.studio-app .studio-document-tools label { display: grid; gap: 8px; color: var(--muted); font-size: 12px; font-weight: 650; }
.studio-app .studio-document-tools form div { display: flex; gap: 8px; }
.studio-app .studio-document-tools label,
.studio-app .studio-asset-tools form,
.studio-app .studio-asset-tools label { display: grid; gap: 8px; color: var(--muted); font-size: 12px; font-weight: 650; }
.studio-app .studio-document-tools form div,
.studio-app .studio-asset-tools form div { display: flex; gap: 8px; }
.studio-app .studio-document-tools input,
.studio-app .studio-document-tools select { width: 100%; min-width: 0; min-height: 44px; padding-inline: 12px; border: 1px solid var(--line-strong); border-radius: 5px; background: var(--paper); color: var(--ink); }
.studio-app .studio-document-tools input { flex: 1; }
.studio-app .studio-document-tools select,
.studio-app .studio-asset-tools input { width: 100%; min-width: 0; min-height: 44px; padding-inline: 12px; border: 1px solid var(--line-strong); border-radius: 5px; background: var(--paper); color: var(--ink); }
.studio-app .studio-document-tools input,
.studio-app .studio-asset-tools input { flex: 1; }
.studio-app .studio-result-count { margin: 24px 0 10px; color: var(--muted); font-size: 13px; }
.studio-app .studio-document-row { display: grid; grid-template-columns: 105px minmax(240px, 1fr) minmax(360px, 0.9fr); gap: 24px; align-items: start; padding-block: 26px; border-top: 1px solid var(--line); }
.studio-app .studio-document-list > :first-child { border-top: 0; }