diff --git a/client/src/features/main/domains/DomainCard.tsx b/client/src/features/main/domains/DomainCard.tsx index f3f28bb..9964da8 100644 --- a/client/src/features/main/domains/DomainCard.tsx +++ b/client/src/features/main/domains/DomainCard.tsx @@ -10,18 +10,23 @@ type Props = { }; export function DomainCard({ icon, title, desc, onClick, disabled }: Props) { + const isDisabled = !!disabled; + const buttonLabel = isDisabled ? "설정 완료" : "설정하기"; + return ( -
+
{icon}

{title}

{desc}

); diff --git a/client/src/features/main/domains/domains.module.scss b/client/src/features/main/domains/domains.module.scss index 6e766fe..6487f55 100644 --- a/client/src/features/main/domains/domains.module.scss +++ b/client/src/features/main/domains/domains.module.scss @@ -70,4 +70,21 @@ } - \ No newline at end of file +.buttonDisabled { + color: #757373; + border-color: #d0d3d8; + cursor: default; +} + +.buttonDisabled:hover { + color: #757373; + border-color: #d0d3d8; +} + +.card.disabled, +.card.disabled:hover { + transform: none !important; + box-shadow: 0 10px 30px rgba(2, 6, 23, .06); + background: #ffffff !important; + cursor: default !important; +} \ No newline at end of file diff --git a/client/src/features/notice/NoticeSetting.tsx b/client/src/features/notice/NoticeSetting.tsx index 0a8073e..1ba8465 100644 --- a/client/src/features/notice/NoticeSetting.tsx +++ b/client/src/features/notice/NoticeSetting.tsx @@ -273,7 +273,12 @@ function NoticeCard({ // 편집 중이거나 버튼/인풋 클릭 시에는 선택 동작 안함 if (editing) return; const target = e.target as HTMLElement; - if (target.closest("button") || target.closest("input") || target.closest("a")) return; + if ( + target.closest("button") || + target.closest("input") || + target.closest("a") + ) + return; // 이미 선택된 카드면 선택 해제, 아니면 선택 onSelect(isSelected ? null : setting.domain_id); @@ -281,7 +286,9 @@ function NoticeCard({ return (
@@ -437,7 +444,7 @@ function NoticeCard({ ) : ( <> - {/*
*/} +
{/* 다중 배지 표시 */}
@@ -453,7 +460,7 @@ function NoticeCard({ )}
- {/* 요약 상태 토글 (읽기 전용) */} + {/* 요약 상태 토글 (바로 수정 가능) */}
요약 @@ -461,12 +468,25 @@ function NoticeCard({ {summary ? "ON" : "OFF"}
{ + e.stopPropagation(); // 카드 선택 방지 + const newSummary = !summary; + setSummary(newSummary); + try { + await updateSetting(getId(setting), { summary: newSummary }); + onUpdated({ ...setting, summary: newSummary }); + } catch { + setSummary(!newSummary); // 실패 시 롤백 + show("요약 설정 변경에 실패했습니다."); + } + }} + style={{ cursor: "pointer" }} > - +
@@ -490,7 +510,10 @@ interface NoticeSettingProps { onSelectDomain: (domainId: string | null) => void; } -function NoticeSettingInner({ selectedDomainId, onSelectDomain }: NoticeSettingProps) { +function NoticeSettingInner({ + selectedDomainId, + onSelectDomain, +}: NoticeSettingProps) { const [items, setItems] = useState([]); const [settingsMap, setSettingsMap] = useState>({}); const [domainMap, setDomainMap] = useState>({}); @@ -613,9 +636,15 @@ function NoticeSettingInner({ selectedDomainId, onSelectDomain }: NoticeSettingP ); } -const NoticeSetting: React.FC = ({ selectedDomainId, onSelectDomain }) => ( +const NoticeSetting: React.FC = ({ + selectedDomainId, + onSelectDomain, +}) => ( - + ); diff --git a/client/src/features/notice/RecentNotice.tsx b/client/src/features/notice/RecentNotice.tsx index 3bab1a9..b822bf8 100644 --- a/client/src/features/notice/RecentNotice.tsx +++ b/client/src/features/notice/RecentNotice.tsx @@ -1,5 +1,5 @@ // src/features/notice/RecentNotice.tsx -import React, { useEffect, useMemo, useRef, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import "./RecentNotice.scss"; import "../../../public/assets/style/_flex.scss"; import "../../../public/assets/style/_typography.scss"; @@ -27,6 +27,7 @@ type RecentItem = { const INITIAL_COUNT = 10; const PAGE_SIZE = 5; +const POLLING_INTERVAL = 500; // 0.5초마다 새 알림 확인 const pad2 = (n: number) => (n < 10 ? `0${n}` : String(n)); @@ -113,26 +114,28 @@ const RecentNotice: React.FC = ({ selectedDomainId }) => { const [allItems, setAllItems] = useState([]); const [visibleCount, setVisibleCount] = useState(INITIAL_COUNT); - // 초기 로드: settings + domains - useEffect(() => { - let alive = true; - (async () => { - try { + // 데이터 로드 함수 + const loadData = useCallback(async (isInitial = false) => { + try { + if (isInitial) { setLoading(true); setBanner(null); + } + + const [settings, domainList] = await Promise.all([ + fetchSettings(), + fetchMain(), + ]); - const [settings, domainList] = await Promise.all([ - fetchSettings(), - fetchMain(), - ]); - if (!alive) return; + const flat = flattenAndSortMessages(settings); + setAllItems(flat); + setDomains(domainList); - const flat = flattenAndSortMessages(settings); - setAllItems(flat); - setDomains(domainList); + if (isInitial) { setVisibleCount(Math.min(INITIAL_COUNT, flat.length)); - } catch (e: any) { - if (!alive) return; + } + } catch (e: any) { + if (isInitial) { setBanner({ type: "error", text: e?.message || "최근 알림을 불러오지 못했습니다.", @@ -140,15 +143,26 @@ const RecentNotice: React.FC = ({ selectedDomainId }) => { setAllItems([]); setDomains([]); setVisibleCount(0); - } finally { - if (alive) setLoading(false); } - })(); - return () => { - alive = false; - }; + } finally { + if (isInitial) setLoading(false); + } }, []); + // 초기 로드 + useEffect(() => { + loadData(true); + }, [loadData]); + + // 주기적 polling (30초마다) + useEffect(() => { + const interval = setInterval(() => { + loadData(false); + }, POLLING_INTERVAL); + + return () => clearInterval(interval); + }, [loadData]); + // 선택된 도메인으로 필터링 const filteredItems = useMemo(() => { if (!selectedDomainId) return allItems; diff --git a/server/src/crawl/webCrawler.ts b/server/src/crawl/webCrawler.ts index cf2c93b..8de02ae 100644 --- a/server/src/crawl/webCrawler.ts +++ b/server/src/crawl/webCrawler.ts @@ -39,7 +39,7 @@ export class WebCrawler { case 'pknu': crawler = new PKNUCrawler(); break; - case 'pknu-notice-watch': + case 'preview--pknu-notice-watch': console.log(`[WebCrawler] TestCrawler 사용: ${url}`); crawler = new TestCrawler(url); break; diff --git a/server/src/data/initialDomains.ts b/server/src/data/initialDomains.ts index 27cd414..e0c4bcb 100644 --- a/server/src/data/initialDomains.ts +++ b/server/src/data/initialDomains.ts @@ -43,7 +43,7 @@ export const initialDomains: InitialDomainData[] = [ _id: "4", name: "테스트", url_list: [ - "https://pknu-notice-watch.lovable.app", + "https://preview--pknu-notice-watch.lovable.app", ], keywords: ["테스트"], setting_ids: [],