feat: 뉴스 필터 트리 (신문사 → 분야) + ai_summary 우선 표시
- 좌측 필터: 신문사 펼침 → 분야별 필터 (News/경향신문/문화) - API: source 파라미터 '신문사' 또는 '신문사/분야' 지원 - 리스트: ai_summary 있으면 우선, 없으면 extracted_text fallback Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ from typing import Annotated
|
|||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy import select
|
from sqlalchemy import String, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from core.auth import get_current_user
|
from core.auth import get_current_user
|
||||||
@@ -116,7 +116,12 @@ async def list_articles(
|
|||||||
Document.deleted_at == None,
|
Document.deleted_at == None,
|
||||||
)
|
)
|
||||||
if source:
|
if source:
|
||||||
query = query.where(Document.ai_sub_group == source)
|
if '/' in source:
|
||||||
|
# 신문사/분야 형태 → ai_tags로 필터
|
||||||
|
query = query.where(Document.ai_tags.cast(String).contains(source))
|
||||||
|
else:
|
||||||
|
# 신문사만 → ai_sub_group
|
||||||
|
query = query.where(Document.ai_sub_group == source)
|
||||||
if unread_only:
|
if unread_only:
|
||||||
query = query.where(Document.is_read == False)
|
query = query.where(Document.is_read == False)
|
||||||
|
|
||||||
|
|||||||
@@ -20,18 +20,23 @@
|
|||||||
let selectedArticle = $state(null);
|
let selectedArticle = $state(null);
|
||||||
let filterSource = $state('');
|
let filterSource = $state('');
|
||||||
let showUnreadOnly = $state(false);
|
let showUnreadOnly = $state(false);
|
||||||
let sources = $state([]);
|
let sourceTree = $state({}); // { 경향신문: ['문화', '사회'], NYT: ['World'] }
|
||||||
let currentPage = $state(1);
|
let currentPage = $state(1);
|
||||||
let noteEditing = $state(false);
|
let noteEditing = $state(false);
|
||||||
let noteText = $state('');
|
let noteText = $state('');
|
||||||
|
let expandedSources = $state({});
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
const srcData = await api('/news/sources');
|
const srcData = await api('/news/sources');
|
||||||
// 신문사별 유니크
|
const tree = {};
|
||||||
const names = new Set();
|
srcData.forEach(s => {
|
||||||
srcData.forEach(s => names.add(s.name.split(' ')[0]));
|
const paper = s.name.split(' ')[0];
|
||||||
sources = [...names];
|
const cat = s.category || '';
|
||||||
|
if (!tree[paper]) tree[paper] = [];
|
||||||
|
if (cat && !tree[paper].includes(cat)) tree[paper].push(cat);
|
||||||
|
});
|
||||||
|
sourceTree = tree;
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
loadArticles();
|
loadArticles();
|
||||||
});
|
});
|
||||||
@@ -127,11 +132,26 @@
|
|||||||
class="w-full text-left px-2 py-1.5 rounded text-sm mb-1 {filterSource === '' ? 'bg-[var(--accent)]/15 text-[var(--accent)]' : 'text-[var(--text-dim)] hover:bg-[var(--surface)]'}"
|
class="w-full text-left px-2 py-1.5 rounded text-sm mb-1 {filterSource === '' ? 'bg-[var(--accent)]/15 text-[var(--accent)]' : 'text-[var(--text-dim)] hover:bg-[var(--surface)]'}"
|
||||||
>📰 전체</button>
|
>📰 전체</button>
|
||||||
|
|
||||||
{#each sources as src}
|
{#each Object.entries(sourceTree) as [paper, categories]}
|
||||||
<button
|
<div class="mb-0.5">
|
||||||
onclick={() => { filterSource = src; }}
|
<button
|
||||||
class="w-full text-left px-2 py-1.5 rounded text-sm mb-0.5 {filterSource === src ? 'bg-[var(--accent)]/15 text-[var(--accent)]' : 'text-[var(--text-dim)] hover:bg-[var(--surface)]'}"
|
onclick={() => { filterSource = paper; expandedSources[paper] = !expandedSources[paper]; }}
|
||||||
>{src}</button>
|
class="w-full text-left px-2 py-1.5 rounded text-sm flex items-center justify-between
|
||||||
|
{filterSource === paper ? 'bg-[var(--accent)]/15 text-[var(--accent)]' : 'text-[var(--text-dim)] hover:bg-[var(--surface)]'}"
|
||||||
|
>
|
||||||
|
<span>{paper}</span>
|
||||||
|
<span class="text-[10px]">{expandedSources[paper] ? '▼' : '▶'}</span>
|
||||||
|
</button>
|
||||||
|
{#if expandedSources[paper] && categories.length > 0}
|
||||||
|
{#each categories as cat}
|
||||||
|
<button
|
||||||
|
onclick={() => { filterSource = `${paper}/${cat}`; }}
|
||||||
|
class="w-full text-left pl-6 pr-2 py-1 rounded text-xs
|
||||||
|
{filterSource === `${paper}/${cat}` ? 'bg-[var(--accent)]/15 text-[var(--accent)]' : 'text-[var(--text-dim)] hover:bg-[var(--surface)]'}"
|
||||||
|
>{cat}</button>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<hr class="my-3 border-[var(--border)]">
|
<hr class="my-3 border-[var(--border)]">
|
||||||
|
|||||||
Reference in New Issue
Block a user