From f24d35681fc656318e5421952631b6d3d7d542f6 Mon Sep 17 00:00:00 2001 From: hyungi Date: Sun, 7 Jun 2026 16:57:34 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat(ui):=20=ED=99=88=20=EB=8C=80=EC=8B=9C?= =?UTF-8?q?=EB=B3=B4=EB=93=9C=20=EB=8D=B0=EC=9D=BC=EB=A6=AC=20=ED=99=88=20?= =?UTF-8?q?cockpit=20=EC=9E=AC=EC=84=A4=EA=B3=84=20(=EC=95=881=20=EA=B3=A8?= =?UTF-8?q?=EA=B2=A9+=EC=95=882=20=EC=9C=84=EC=A0=AF+=EC=95=883=20?= =?UTF-8?q?=EB=B6=84=ED=8F=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 확정 시안 dashboard-sage-3 의 권장 합성(안1 데일리 홈 골격 + 안2 검토/파이프라인 위젯 + 안3 도메인 분포 한 줄)으로 콘텐츠 재구조화. F1 세이지 테마 위 레이아웃 개편. - 인사 헤더 + 오늘 요약 띠(검토 대기 + 디제스트 톱 + 스탯 띠) - 2열: 좌(빠른 캡처·활동 타임라인) / 우(학습·도메인 분포+파이프라인 칩·고정) - digest/도메인 분포는 기존 엔드포인트 wiring(백엔드 변경 0), 학습 streak는 링크형 degrade Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/routes/+page.svelte | 711 ++++++++++++++----------------- 1 file changed, 312 insertions(+), 399 deletions(-) diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 2b8d6a1..a92eb8d 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -1,7 +1,7 @@ -
-
+
+
- -
-

대시보드

- {#if systemView} - - {systemView.label} - - - {/if} + +
+

안녕하세요, {greetingName}

+ 오늘도 지식 쌓는 날.
+
{todayLabel}
{#if loading} -
- {#each Array(4) as _} - - {/each} +
+ + + +
+
+
+
+
+
+
+
+
+
- - {:else if summary} - - {#if pinnedMemos.length > 0} -
- {#each pinnedMemos as memo (memo.id)} -
- - - - {memo.title && memo.title !== memo.content?.split('\n')[0]?.replace(/^#+\s*/, '').slice(0, 80) - ? memo.title - : memo.content?.split('\n')[0] || '메모'} - - - - - -
handlePinCheckbox(e, memo)} - > -
- {@html renderMemoHtml(memo.content || '', { - compact: true, - interactive: true, - taskStates: memo.memo_task_state ?? {}, - now: nowTick, - })} -
-
- {#if countHiddenTasks(memo.memo_task_state, nowTick, DEFAULT_HIDE_AFTER_MS) > 0 || showHiddenByMemo[memo.id]} - - {/if} - 메모함에서 보기 → -
+ +
+ +
+ +
+ + {summary.inbox_count.toLocaleString()} + + 검토 대기 문서 + {#if summary.inbox_count > 0} + 검토 시작 → + {:else} + 미분류 없음 + {/if} +
+ + + {#if digestLead} + +
+ 속보 + {digestLead.date} 브리핑
-
- {/each} - {#if pinnedMemos.length >= 3} - 더보기 → +
+ {digestLead.topic_label} +
+
+ 관련 기사 {digestLead.article_count}건 + · 중요도 {digestLead.importance_score.toFixed(2)} + · {countryKo(digestLead.country)} +
+ + {:else} + + 오늘의 뉴스 브리핑 보기 → + {/if}
- {/if} - -
- - - -
-

문서함

- -
-

{(summary.documents_count ?? 0).toLocaleString()}

-

- {#if summary.today_added > 0} - +{summary.today_added} 오늘 - {:else} - 일반 문서 - {/if} -

-
-
- - - - -
-

메모

- -
-

{(summary.memos_count ?? 0).toLocaleString()}

-

직접 작성

-
-
- - - - -
-

뉴스

- -
-

{(summary.news_count ?? 0).toLocaleString()}

-

수집 기사

-
-
- - - - -
-

승인 대기

- -
-

- {summary.inbox_count} -

- {#if summary.inbox_count > 0} -

검토하기 →

- {:else} -

미분류 없음

- {/if} -
-
+ +
+ {@render stat((summary.documents_count ?? 0).toLocaleString(), '문서', 'text-accent')} + {@render stat((summary.news_count ?? 0).toLocaleString(), '뉴스')} + {#if domainTotal > 0} + {@render stat(domainCount('Industrial_Safety').toLocaleString(), '산업안전', 'text-domain-safety')} + {@render stat(domainCount('Engineering').toLocaleString(), '엔지니어링', 'text-domain-engineering')} + {/if} + {#if summary.category_counts?.library} + {@render stat(summary.category_counts.library.toLocaleString(), '자료실')} + {/if} + {@render stat((summary.memos_count ?? 0).toLocaleString(), '메모')} +
- - {#if summary.category_counts && (Object.keys(summary.category_counts).length > 0 || summary.library_pending_suggestions > 0)} -
- {#each CATEGORY_CARDS as cat} - {@const count = summary.category_counts?.[cat.key] ?? 0} - - -
-

{cat.label}

- -
-

{count.toLocaleString()}

-

카테고리

-
-
- {/each} + + - {/if} + +
- - {#if summary.tier_health && summary.tier_health.triage_total > 0} - {@const th = summary.tier_health} - {@const esc_rate = th.triage_total > 0 ? th.escalated_total / th.triage_total : 0} - {@const json_rate = th.triage_total > 0 ? th.triage_json_invalid / th.triage_total : 0} - {@const sup_rate = th.triage_total > 0 ? th.suppressed_total / th.triage_total : 0} - {@const deep_total = th.deep_total ?? 0} - {@const deep_err_rate = deep_total > 0 ? (th.deep_err_total ?? 0) / deep_total : 0} - - {@const esc_tone = esc_rate < 0.80 ? 'text-error' : 'text-text'} - {@const json_tone = json_rate > 0.05 ? 'text-error' : 'text-text'} - {@const sup_tone = sup_rate > 0.10 ? 'text-warning' : 'text-text'} - {@const deep_tone = deep_err_rate > 0.05 ? 'text-error' : 'text-text'} -
- - -
-

에스컬레이션 비율 (24h)

- + +
+
빠른 캡처
+
+ +
-

- {(esc_rate * 100).toFixed(1)}% -

-

- {th.escalated_total} / {th.triage_total} - {#if esc_rate < 0.80}(매칭 실패 증가){/if} -

-

safety 정책상 95~100% 가 정상

- {#if Object.keys(th.escalation_by_reason).length > 0} -
- {#each Object.entries(th.escalation_by_reason).slice(0, 4) as [reason, n]} - - {reason} {n} - + +
+ + +
+
+ 최근 활동 +
+ {#if summary.law_alerts > 0} + + 법령 {summary.law_alerts} + + {/if} + 전체 보기 → +
+
+ + {#if summary.recent_documents.length > 0} + - {/if} - - - - -
-

triage JSON 건강도 (24h)

- -
-

- {(json_rate * 100).toFixed(1)}% -

-

- 깨짐 {th.triage_json_invalid} 건 - {#if json_rate > 0.05}(프롬프트 이슈 의심){/if} -

-

5% 초과 시 4B 프롬프트·모델 재검토

-
- - - -
-

Backlog Suppression (24h)

- -
-

- {(sup_rate * 100).toFixed(1)}% -

-

- 억제 {th.suppressed_total} 건 - {#if sup_rate > 0.10}(임계치 재조정 신호){/if} -

-

10% 초과 시 ratio/pending threshold 조정

-
- - - -
-

Deep summary 안정성 (24h)

- -
-

- {(deep_err_rate * 100).toFixed(1)}% -

-

- 실패 {th.deep_err_total ?? 0} / {deep_total} - {#if deep_err_rate > 0.05}(MLX 안정성 점검){/if} -

-

call_failed / parse:* 합계, 5% 초과 시 점검

-
-
- {/if} - - - -
-

최근 활동

-
- {#if summary.law_alerts > 0} - - 법령 {summary.law_alerts} - + {:else} + {/if}
- {#if summary.recent_documents.length > 0} - - {:else} - - {/if} -
+ +
- + + +
+ 학습 + +
+
암기 노트 학습 시작 →
+
검수함 · 복습함 · 암기카드
+
+ + + {#if domainDist.length > 0} +
+
도메인 분포
+
전체 {domainTotal.toLocaleString()}
+ + +
+ {#each domainDist as d (d.name)} +
+ {/each} +
+ +
+ {#each domainDist.slice(0, 6) as d (d.name)} + + + {domainLabel(d.name)} + {d.count.toLocaleString()} + + {/each} +
+ + +
+ 파이프라인 + {#if totalFailed > 0}실패 {totalFailed}{/if} + {#if totalPending > 0}대기 {totalPending}{/if} + {#if totalProcessing > 0}처리중 {totalProcessing}{/if} + {#if totalFailed === 0 && totalPending === 0 && totalProcessing === 0}정상{/if} +
+
+ {/if} + + + {#if pinnedMemos.length > 0} +
+
+ 고정 항목 + 관리 → +
+
+ {#each pinnedMemos as memo (memo.id)} + + 메모 + {pinTitle(memo)} + + + {/each} +
+
+ {/if} +
+
+ +
{ if (!e.currentTarget.open) pipelineManualClosed = true; }} > - + - 파이프라인 + 파이프라인 상세 - {#if totalFailed > 0} - 실패 {totalFailed} - {/if} - {#if totalPending > 0} - 대기 {totalPending} - {/if} - {#if totalFailed === 0 && totalPending === 0} - 처리 완료 - {/if} + {#if totalFailed > 0}실패 {totalFailed}{/if} + {#if totalPending > 0}대기 {totalPending}{/if} + {#if totalFailed === 0 && totalPending === 0}처리 완료{/if} -
+

최근 24시간

{#if pipelineRows.length > 0}
@@ -522,9 +445,7 @@ {row.label} {#if row.oldestPendingAgeSec && row.oldestPendingAgeSec > 600} - - ({formatAge(row.oldestPendingAgeSec)}) - + ({formatAge(row.oldestPendingAgeSec)}) {/if} @@ -551,22 +472,14 @@
+{#snippet stat(value: string, label: string, colorClass = 'text-text')} +
+ {value} + {label} +
+{/snippet} + -- 2.52.0 From 36c6ff8046b60e2a85bea89abc4a00c7f5fbe18e Mon Sep 17 00:00:00 2001 From: hyungi Date: Sun, 7 Jun 2026 17:15:27 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat(ui):=20=EB=AC=B8=EC=84=9C=20/documents?= =?UTF-8?q?=20DEVONthink=20=EC=BB=AC=EB=9F=BC=20=EB=B8=8C=EB=9D=BC?= =?UTF-8?q?=EC=9A=B0=EC=A0=80=20=EC=A0=84=EB=A9=B4=20=EC=9E=AC=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20(3-pane=20+=20=EC=9D=B8=EC=8A=A4=ED=8E=99=ED=84=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 확정 시안 documents-confirmed-column-browser 대로 세로 split → 가로 3-pane 재구조화. - 좌: 리스트 컬럼(제목+도메인 / 형식 배지 / 수정일, 제목·수정 정렬, zebra, 선택강조) - 중앙: 리더(DocumentViewer 재사용) + 상단 ⓘ 인스펙터 토글·모바일 뒤로가기 - 우: 인스펙터 인라인(정보 KV · 태그 · See Also · AI 분류, ⓘ 토글) - 모바일: 흐름형(리스트 → 풀스크린 리더 → 정보 Drawer 시트) 기존 검색·모드·AI답변·필터칩·일괄작업(도메인/태그/삭제)·키보드내비·업로드·페이지네이션 전부 흡수. See Also(벡터 유사도)는 엔드포인트 부재(코드 TODO)로 degrade — eid 세션 후 백엔드. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/routes/documents/+page.svelte | 881 ++++++++------------- 1 file changed, 331 insertions(+), 550 deletions(-) diff --git a/frontend/src/routes/documents/+page.svelte b/frontend/src/routes/documents/+page.svelte index 567708c..bc187f4 100644 --- a/frontend/src/routes/documents/+page.svelte +++ b/frontend/src/routes/documents/+page.svelte @@ -1,13 +1,15 @@ -
- -
- -
- +
+ + +
- -
+ +
{ e.stopPropagation(); toggleRowSelect(doc.id, e.currentTarget.checked); }} + class="mt-1 shrink-0 accent-accent {selectedIds.size > 0 ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'} transition-opacity" + aria-label="선택" + /> + +
+
{doc.title || '제목 없음'}
+
+ {domainLabel(doc.ai_domain)} + {shortDate(doc.updated_at || doc.created_at)} +
+
+ {badge.label} +
+ {/each} + + {#if !searchResults && totalPages > 1} +
+ {#each Array(totalPages) as _, i} + + {/each} +
+ {/if} + {/if} +
+
+ + +
+ {#if selectedDoc} + +
+
- - - +
- {/if} - - -
- - {#if !loading} -
- {total}건 +
+
- {/if} - - - {#if showAskCard} -
+ {:else if showAskCard} +
{ askDismissed = true; }} />
- {/if} - - - {#if loading} -
- {#each Array(5) as _} -
- {/each} -
- {:else if items.length === 0} -
- {#if searchQuery} -

'{searchQuery}'에 대한 결과가 없습니다

- - {:else if hasActiveFilters} -

이 분류에 문서가 없습니다

- - {:else} -

등록된 문서가 없습니다

- {/if} -
{:else} - {#if viewMode === 'table'} - - {:else} -
- {#each items as doc} -
- -
- {/each} -
- {/if} - - {#if !searchResults && totalPages > 1} -
- {#each Array(totalPages) as _, i} - - {/each} -
- {/if} + {/if} -
- - {#if selectedDoc} -
- -
- {/if} -
- - - - {#if selectedDoc} -
- + {#if selectedDoc} - ui.closeDrawer()} - ondelete={handleDocDelete} - /> +
{@render inspector(selectedDoc)}
{/if}
- +

{selectionCount}건의 문서에 새 도메인을 지정합니다.

- {#snippet footer()} - + {/snippet}

{selectionCount}건의 문서에 태그를 추가합니다. 이미 같은 태그가 있으면 건너뜁니다.

- + {#snippet footer()} - + {/snippet}
@@ -918,3 +648,54 @@ loading={bulkBusy} onconfirm={bulkDelete} /> + + +{#snippet inspector(doc)} + {@const fmt = formatBadge(doc.file_format)} + {@const size = fileSizeLabel(doc.file_size)} +
+ +
+
정보
+
+
종류{fmt.label}
+ {#if doc.ai_domain}
도메인{domainLabel(doc.ai_domain)}
{/if} + {#if doc.ai_sub_group}
하위{doc.ai_sub_group}
{/if} +
수정{shortDate(doc.updated_at || doc.created_at)}
+ {#if size}
원본{size}
{/if} + {#if doc.md_status}
md 상태{doc.md_status}
{/if} + {#if doc.read_count}
읽음{doc.read_count}회
{/if} +
+
+ + + {#if doc.ai_tags && doc.ai_tags.length > 0} +
+
태그
+
+ {#each doc.ai_tags as t} + + {/each} +
+
+ {/if} + + +
+
See Also (관련 문서)
+

벡터 유사도 기반 관련 문서 — 준비 중

+
+ + + {#if doc.ai_domain} +
+
AI 분류
+
+ 도메인 {domainLabel(doc.ai_domain)}{#if doc.ai_sub_group} · 하위 {doc.ai_sub_group}{/if} + {#if doc.ai_confidence != null}
신뢰도 {doc.ai_confidence.toFixed(2)}{/if} + {#if doc.ai_analysis_tier}· {doc.ai_analysis_tier}{/if} +
+
+ {/if} +
+{/snippet} -- 2.52.0 From 73d7683eda6aa361a5a4dca42cd8159a1d24bb68 Mon Sep 17 00:00:00 2001 From: hyungi Date: Sun, 7 Jun 2026 19:39:09 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat(ui):=20=EB=AA=A8=EB=8B=9D=EB=B8=8C?= =?UTF-8?q?=EB=A6=AC=ED=95=91=20/news=20=ED=8E=B8=EC=A7=91=20=EC=8B=A0?= =?UTF-8?q?=EB=AC=B8=201=EB=A9=B4=20=EC=9E=AC=EC=9E=91=EC=84=B1=20(?= =?UTF-8?q?=EA=B5=AD=EA=B0=80=20=EC=83=89=EC=B9=A9=C2=B7=EC=9D=B4=EB=AA=A8?= =?UTF-8?q?=EC=A7=80=20=EC=A0=9C=EA=B1=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 확정 시안 morning-briefing-final 의 '편집 신문 1면'으로 재구조화. - 마스트헤드(제호·날짜선택·에디션메타·오늘의 한 줄 deck·통계·상태 가드 배너) - 리드 토픽 전체너비(관점 2열) + 나머지 2열 그리드, folio/serif 헤드라인 - 국가별 관점(색칩+기사ID 링크+요약)·차이/공통 ednote·인용(serif)·지난 흐름 - 이모지 국기 → 국가 색칩(no-emoji 규칙). 읽음/별표/날짜 등 전 기능 보존. 데이터·API(/briefing)는 기존 그대로. 기존 news lint:tokens 51 위반도 해소. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/routes/news/+page.svelte | 504 ++++++++++++-------------- 1 file changed, 239 insertions(+), 265 deletions(-) diff --git a/frontend/src/routes/news/+page.svelte b/frontend/src/routes/news/+page.svelte index 1dfcc10..fed06d4 100644 --- a/frontend/src/routes/news/+page.svelte +++ b/frontend/src/routes/news/+page.svelte @@ -1,101 +1,59 @@ -
-
-
-

야간 뉴스 브리핑

- {#if availableDates.length > 0} -
- - + + {#each availableDates as d} + + {/each} + + {:else if briefing} + {briefing.briefing_date} + {/if} + {#if briefing} + {briefing.total_topics}토픽{#if highlightedCount > 0} · 별표 {highlightedCount}{/if} + 새벽 수집 + {/if} +
+
+ + {#if briefing?.headline_oneliner} +
+ 오늘의 한 줄 + {briefing.headline_oneliner} +
+ {/if} + + {#if briefing} +
+
+ {briefing.total_articles} + 총 기사 +
+
+ {briefing.total_countries} + 개국 +
+
+ {briefing.total_topics} + 토픽 +
+
+ {/if} + + {#if briefing && (briefing.status === 'partial' || briefing.status === 'failed')} +
+ + {#if briefing.status === 'failed'} + LLM 분석 실패율이 높습니다 ({briefing.llm_failures}/{briefing.llm_calls}, {fallbackPct}%). 일부 토픽이 원문 묶음으로 표시됩니다. + {:else} + 일부 토픽 LLM 실패 ({briefing.llm_failures}/{briefing.llm_calls}). 다른 토픽은 정상 분석되었습니다. + {/if} +
+ {/if} +
+ + + {#if loading} +
불러오는 중…
+ {:else if errorMsg} +
{errorMsg}
+ {:else if briefing} + {#if briefing.status === 'empty'} +
+

오늘 새벽({briefing.briefing_date}) 다국 비교 가능한 토픽이 없습니다.

+

(수집 뉴스 0건 또는 2개국 이상 다룬 주제 없음)

+
+ {:else} + + {#if leadTopic} + {@render topicCard(leadTopic, true)} + {/if} + + {#if restTopics.length > 0} +
+ {#each restTopics as topic (topic.id)} + {@render topicCard(topic, false)} {/each} - +
+ {/if} + {/if} + {/if} +
+
+ +{#snippet topicCard(topic, isLead)} +
+ +
+
{folio(topic.topic_rank)}
+
+
+ {topic.topic_label}{#if topic.llm_fallback_used}(원문 묶음){/if} +
+
{topic.headline}
+
+ {topic.country_count}개국{topic.article_count}건 +
+
+
+ + +
+
+ + +
+ {#if topic.country_perspectives.length > 0} +
국가별 관점
+
+ {#each topic.country_perspectives as cp} +
+
+ {countryLabel(cp.country)} + {#if cp.article_ids.length > 0} + + {#each cp.article_ids as id} + #{id} + {/each} + + {/if} +
+
{cp.summary}
+
+ {/each} +
+ {/if} + + {#if topic.divergences.length > 0 || topic.convergences.length > 0} +
+ {#if topic.divergences.length > 0} +
+ 차이 + {topic.divergences.join(' · ')} +
+ {/if} + {#if topic.convergences.length > 0} +
+ 공통 + {topic.convergences.join(' · ')} +
+ {/if} +
+ {/if} + + {#if topic.key_quotes.length > 0} +
+ {#each topic.key_quotes as q} +
+
{q.quote}
+
{countryLabel(q.country)} · {q.source}
+
+ {/each} +
+ {/if} + + {#if topic.historical_context} +
+ 지난 흐름{topic.historical_context}
{/if}
-

- {#if briefing} - {briefing.briefing_date} 새벽 수집 · 총 {briefing.total_articles}건 / {briefing.total_countries}개국 / {briefing.total_topics}개 토픽 - {:else} - 매일 KST 자정~05:00 누적 뉴스를 주제별로 다국 비교 분석합니다. - {/if} -

- +
+{/snippet} - {#if loading} - -

불러오는 중…

-
- {:else if errorMsg} - -

{errorMsg}

-
- {:else if briefing} - {#if briefing.status === 'empty'} - -

- 오늘 새벽({briefing.briefing_date}) 다국 비교 가능한 토픽이 없습니다. -

-

- (수집 뉴스 0건 또는 2개국 이상 다룬 주제 없음) -

-
- {:else} - {#if briefing.status === 'failed'} -
- ⚠ LLM 분석 실패율이 높습니다 ({briefing.llm_failures}/{briefing.llm_calls}, {fallbackPct}%). 일부 토픽이 원문 묶음으로 표시됩니다. -
- {:else if briefing.status === 'partial'} -
- 일부 토픽 LLM 실패 ({briefing.llm_failures}/{briefing.llm_calls}). 다른 토픽은 정상 분석되었습니다. -
- {/if} - - {#each briefing.topics as topic (topic.id)} -
- -
-
- #{topic.topic_rank} -
-

- {topic.topic_label} - {#if topic.llm_fallback_used} - (원문 묶음) - {/if} -

-

{topic.headline}

-

- {topic.country_count}개국 · {topic.article_count}건 -

-
-
- - -
-
- - {#if topic.country_perspectives.length > 0} -
- {#each topic.country_perspectives as cp} -
- {countryLabel(cp.country)} - · - {cp.summary} - {#if cp.article_ids.length > 0} - - {#each cp.article_ids as id, i} - {#if i > 0}·{/if}#{id} - {/each} - - {/if} -
- {/each} -
- {/if} - - {#if topic.divergences.length > 0} -
- 차이 - {topic.divergences.join(' · ')} -
- {/if} - - {#if topic.convergences.length > 0} -
- 공통 - {topic.convergences.join(' · ')} -
- {/if} - - {#if topic.key_quotes.length > 0} -
    - {#each topic.key_quotes as q} -
  • - {countryLabel(q.country)} · {q.source} - "{q.quote}" -
  • - {/each} -
- {/if} - - {#if topic.historical_context} -

- ↩ 지난 흐름 · {topic.historical_context} -

- {/if} -
-
-
- {/each} - {/if} - {/if} -
+ -- 2.52.0 From 058183d3ff28b78817cdbfe96e8f0db44423fd2b Mon Sep 17 00:00:00 2001 From: hyungi Date: Sun, 7 Jun 2026 19:41:21 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat(ui):=20/digest=20=EC=9B=9C=20=ED=81=B4?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=20=E2=86=92=20=EC=84=B8=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=9E=AC=ED=86=A4=20(=EC=95=B1=20=ED=86=A4=20=ED=86=B5?= =?UTF-8?q?=EC=9D=BC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 편집형 digest 가 자체 웜 클레이 팔레트라 세이지 앱 속 '웜 섬'이었던 것을 세이지로 통일. 스코프 diff --git a/frontend/src/routes/documents/+page.svelte b/frontend/src/routes/documents/+page.svelte index bc187f4..0aad960 100644 --- a/frontend/src/routes/documents/+page.svelte +++ b/frontend/src/routes/documents/+page.svelte @@ -231,8 +231,15 @@ goto(`/documents${qs ? '?' + qs : ''}`, { noScroll: true }); } - function selectDoc(doc) { - selectedDoc = selectedDoc?.id === doc.id ? null : doc; + async function selectDoc(doc) { + if (selectedDoc?.id === doc.id) { selectedDoc = null; return; } + selectedDoc = doc; // 즉시 표시(리더 + 기본 인스펙터) + // 인스펙터 풀 메타 하이드레이션 — 검색 결과(SearchResult)는 메타가 빈약(태그/크기/하위/md상태/읽음 없음). + // 풀 문서를 조회해 채운다(기존 GET /documents/{id}, 백엔드 무변). 리스트 모드도 md상태 등 보강. + try { + const full = await api(`/documents/${doc.id}`); + if (selectedDoc?.id === doc.id) selectedDoc = { ...doc, ...full }; + } catch { /* 실패 시 기본 정보 유지 */ } } // bulk 선택 @@ -271,7 +278,13 @@ if (!tag) return; await runBulk('태그 추가', async (id) => { const doc = sortedItems.find((d) => d.id === id); - const existing = Array.isArray(doc?.ai_tags) ? doc.ai_tags : []; + // ★ 검색 결과(SearchResult)는 ai_tags 가 없음 → 빈 배열로 PATCH 하면 기존 태그 전체 유실(덮어쓰기). + // 미확인이면 풀 문서를 조회해 실제 태그를 머지(데이터 손실 방지). + let existing = Array.isArray(doc?.ai_tags) ? doc.ai_tags : null; + if (existing == null) { + try { const full = await api(`/documents/${id}`); existing = Array.isArray(full?.ai_tags) ? full.ai_tags : []; } + catch { return; } // 현재 태그를 못 읽으면 덮어쓰지 않고 건너뜀 + } if (existing.includes(tag)) return; return api(`/documents/${id}`, { method: 'PATCH', body: JSON.stringify({ ai_tags: [...existing, tag] }) }); }); @@ -282,12 +295,6 @@ await runBulk('삭제', (id) => api(`/documents/${id}?delete_file=true`, { method: 'DELETE' })); } - function handleDocDelete() { - selectedDoc = null; - if (ui.isDrawerOpen('meta')) ui.closeDrawer(); - loadDocuments(); - } - function handleKeydown(e) { if (e.key === 'Escape') { if (selectedDoc && !ui.isDrawerOpen('meta') && !isXl.current) { selectedDoc = null; return; } @@ -298,7 +305,8 @@ let totalPages = $derived(Math.ceil(total / 50)); let items = $derived(searchResults || documents); let sortedItems = $derived.by(() => { - if (!sortKey) return items; + // 검색 모드는 서버 관련도순 유지 + SearchResult 에 updated_at 부재(NaN 비교=무의미) → 클라 정렬 비활성 + if (!sortKey || searchResults) return items; const arr = [...items]; const dir = sortDir === 'asc' ? 1 : -1; arr.sort((a, b) => { @@ -497,14 +505,21 @@
- - - {#if !loading}{total}건{/if} - + {#if searchResults} + + 관련도순 + + {#if !loading}{total}건{/if} + {:else} + + + {#if !loading}{total}건{/if} + + {/if}
diff --git a/frontend/src/routes/news/+page.svelte b/frontend/src/routes/news/+page.svelte index fed06d4..7f92075 100644 --- a/frontend/src/routes/news/+page.svelte +++ b/frontend/src/routes/news/+page.svelte @@ -205,6 +205,9 @@ {isLead ? 'mt-4' : ''} {topic.highlighted ? 'border-accent ring-2 ring-accent/25' : 'border-default'} {topic.is_read ? 'opacity-50 hover:opacity-80' : ''}"> + {#if topic.is_read} + 읽음 + {/if}
{q.quote}
-
{countryLabel(q.country)} · {q.source}
+
+ {#if q.country}{countryLabel(q.country)}{/if} + {q.source} +
{/each}
-- 2.52.0