Skip to content

Commit e7e74ba

Browse files
committed
feat: 오류 토스트 노출
1 parent d17a197 commit e7e74ba

6 files changed

Lines changed: 94 additions & 18 deletions

File tree

src/App.tsx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,13 @@ const TimelineSection = ({
403403
const actionsDisabled = timelineType === "notifications";
404404
const emptyMessage = timelineType === "notifications" ? "표시할 알림이 없습니다." : "표시할 글이 없습니다.";
405405

406+
useEffect(() => {
407+
if (!timeline.error) {
408+
return;
409+
}
410+
showToast(timeline.error, { tone: "error" });
411+
}, [showToast, timeline.error]);
412+
406413
useEffect(() => {
407414
const el = scrollRef.current;
408415
if (!el) {
@@ -475,6 +482,13 @@ const TimelineSection = ({
475482
refreshNotifications();
476483
}, [notificationsOpen, refreshNotifications]);
477484

485+
useEffect(() => {
486+
if (!notificationsError) {
487+
return;
488+
}
489+
showToast(notificationsError, { tone: "error" });
490+
}, [notificationsError, showToast]);
491+
478492
const handleToggleFavourite = async (status: Status) => {
479493
if (!account) {
480494
onError("계정을 선택해주세요.");
@@ -649,7 +663,6 @@ const TimelineSection = ({
649663
<div className="overlay-backdrop" aria-hidden="true" />
650664
<div ref={notificationMenuRef} className="notification-popover panel" role="dialog" aria-modal="true" aria-label="알림">
651665
<div className="notification-popover-body" ref={notificationScrollRef}>
652-
{notificationsError ? <p className="error">{notificationsError}</p> : null}
653666
{notificationItems.length === 0 && !notificationsLoading ? (
654667
<p className="empty">표시할 알림이 없습니다.</p>
655668
) : null}
@@ -785,7 +798,6 @@ const TimelineSection = ({
785798
</div>
786799
<div className="timeline-column-body" ref={scrollRef}>
787800
{!account ? <p className="empty">계정을 선택하면 타임라인을 불러옵니다.</p> : null}
788-
{account && timeline.error ? <p className="error">{timeline.error}</p> : null}
789801
{account && timeline.items.length === 0 && !timeline.loading ? (
790802
<p className="empty">{emptyMessage}</p>
791803
) : null}
@@ -892,6 +904,7 @@ export const App = () => {
892904
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
893905
const [mobileComposeOpen, setMobileComposeOpen] = useState(false);
894906
const { services, accountsState } = useAppContext();
907+
const { showToast } = useToast();
895908
const [sections, setSections] = useState<TimelineSectionConfig[]>(() => {
896909
try {
897910
const raw = localStorage.getItem(SECTION_STORAGE_KEY);
@@ -960,6 +973,14 @@ export const App = () => {
960973
const previousAccountIds = useRef<Set<string>>(new Set());
961974
const hasAccounts = accountsState.accounts.length > 0;
962975

976+
useEffect(() => {
977+
if (!actionError) {
978+
return;
979+
}
980+
showToast(actionError, { tone: "error" });
981+
setActionError(null);
982+
}, [actionError, showToast]);
983+
963984
const registerTimelineListener = useCallback((accountId: string, listener: (status: Status) => void) => {
964985
const next = new Map(timelineListeners.current);
965986
const existing = next.get(accountId) ?? new Set();
@@ -1673,7 +1694,6 @@ export const App = () => {
16731694
{hasAccounts ? (
16741695
<section className="main-column">
16751696
{oauthLoading ? <p className="empty">OAuth 인증 중...</p> : null}
1676-
{actionError ? <p className="error">{actionError}</p> : null}
16771697
{route === "home" ? (
16781698
<section className="panel">
16791699
{sections.length > 0 ? (

src/ui/components/AccountAdd.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,23 @@ import React, { useState } from "react";
22
import type { OAuthClient } from "../../services/OAuthClient";
33
import { normalizeInstanceUrl } from "../utils/account";
44
import { createOauthState, loadRegisteredApp, saveRegisteredApp, storePendingOAuth } from "../utils/oauth";
5+
import { useToast } from "../state/ToastContext";
56

67
export const AccountAdd = ({
78
oauth
89
}: {
910
oauth: OAuthClient;
1011
}) => {
1112
const [instanceUrl, setInstanceUrl] = useState("");
12-
const [error, setError] = useState<string | null>(null);
1313
const [loading, setLoading] = useState(false);
1414
const [showForm, setShowForm] = useState(false);
15+
const { showToast } = useToast();
1516
const handleSubmit = async (event: React.FormEvent) => {
1617
event.preventDefault();
17-
setError(null);
1818

1919
const normalizedUrl = normalizeInstanceUrl(instanceUrl);
2020
if (!normalizedUrl) {
21-
setError("서버 주소를 입력해주세요.");
21+
showToast("서버 주소를 입력해주세요.", { tone: "error" });
2222
return;
2323
}
2424

@@ -42,7 +42,7 @@ export const AccountAdd = ({
4242
const authorizeUrl = oauth.buildAuthorizeUrl(registered, state);
4343
window.location.assign(authorizeUrl);
4444
} catch (err) {
45-
setError(err instanceof Error ? err.message : "OAuth 연결에 실패했습니다.");
45+
showToast(err instanceof Error ? err.message : "OAuth 연결에 실패했습니다.", { tone: "error" });
4646
} finally {
4747
setLoading(false);
4848
}
@@ -83,7 +83,6 @@ export const AccountAdd = ({
8383
onChange={(event) => setInstanceUrl(event.target.value)}
8484
/>
8585
</label>
86-
{error ? <p className="error">{error}</p> : null}
8786
<button type="submit" disabled={loading}>
8887
{loading ? "연결 중..." : "OAuth로 연결"}
8988
</button>

src/ui/components/ComposeBox.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Account, Visibility } from "../../domain/types";
33
import type { MastodonApi } from "../../services/MastodonApi";
44
import { useEmojiManager, type EmojiItem } from "../hooks/useEmojiManager";
55
import { useImageZoom } from "../hooks/useImageZoom";
6+
import { useToast } from "../state/ToastContext";
67
import {
78
calculateCharacterCount,
89
getCharacterLimit,
@@ -88,6 +89,8 @@ export const ComposeBox = ({
8889
} = useImageZoom(imageContainerRef, imageRef);
8990
const [emojiPanelOpen, setEmojiPanelOpen] = useState(false);
9091
const [recentOpen, setRecentOpen] = useState(true);
92+
const { showToast } = useToast();
93+
const lastEmojiErrorRef = useRef<string | null>(null);
9194

9295
// 문자 수 관련 상태
9396
const [characterLimit, setCharacterLimit] = useState<number | null>(null);
@@ -108,6 +111,19 @@ export const ComposeBox = ({
108111
searchEmojis
109112
} = useEmojiManager(account, api, false);
110113

114+
useEffect(() => {
115+
if (emojiStatus !== "error") {
116+
lastEmojiErrorRef.current = null;
117+
return;
118+
}
119+
const message = emojiError ?? "이모지를 불러오지 못했습니다.";
120+
if (message === lastEmojiErrorRef.current) {
121+
return;
122+
}
123+
lastEmojiErrorRef.current = message;
124+
showToast(message, { tone: "error" });
125+
}, [emojiError, emojiStatus, showToast]);
126+
111127
const activeImage = useMemo(
112128
() => attachments.find((item) => item.id === activeImageId) ?? null,
113129
[attachments, activeImageId]
@@ -200,7 +216,7 @@ export const ComposeBox = ({
200216

201217
// 문자 수 제한 검사
202218
if (characterLimit && currentCharCount > characterLimit) {
203-
alert(`글자 수 제한(${characterLimit.toLocaleString()}자)을 초과했습니다.`);
219+
showToast(`글자 수 제한(${characterLimit.toLocaleString()}자)을 초과했습니다.`, { tone: "error" });
204220
return;
205221
}
206222

src/ui/components/ProfileModal.tsx

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,30 @@ export const ProfileModal = ({
295295
};
296296
}, [account, api, targetAccountId]);
297297

298+
useEffect(() => {
299+
if (!profileError) {
300+
return;
301+
}
302+
showToast(profileError, { tone: "error" });
303+
setProfileError(null);
304+
}, [profileError, showToast]);
305+
306+
useEffect(() => {
307+
if (!followError) {
308+
return;
309+
}
310+
showToast(followError, { tone: "error" });
311+
setFollowError(null);
312+
}, [followError, showToast]);
313+
314+
useEffect(() => {
315+
if (!itemsError) {
316+
return;
317+
}
318+
showToast(itemsError, { tone: "error" });
319+
setItemsError(null);
320+
}, [itemsError, showToast]);
321+
298322
const updateItem = useCallback((next: Status) => {
299323
setItems((current) => current.map((item) => (item.id === next.id ? next : item)));
300324
}, []);
@@ -605,7 +629,6 @@ export const ProfileModal = ({
605629
const message = error instanceof Error ? error.message : fallbackMessage;
606630
setRelationship(previous);
607631
setFollowError(message);
608-
showToast(message, { tone: "error" });
609632
} finally {
610633
setFollowLoading(false);
611634
}
@@ -908,9 +931,7 @@ export const ProfileModal = ({
908931
) : null}
909932
</div>
910933
</div>
911-
{followError ? <p className="error">{followError}</p> : null}
912934
{profileLoading ? <p className="empty">프로필을 불러오는 중...</p> : null}
913-
{profileError ? <p className="error">{profileError}</p> : null}
914935
{bioContent
915936
? bioContent.type === "html"
916937
? <div className="profile-bio" dangerouslySetInnerHTML={{ __html: bioContent.value }} />
@@ -929,7 +950,6 @@ export const ProfileModal = ({
929950
</section>
930951
<section className="profile-posts">
931952
<h4>작성한 글</h4>
932-
{itemsError ? <p className="error">{itemsError}</p> : null}
933953
{itemsLoading && items.length === 0 ? <p className="empty">게시글을 불러오는 중...</p> : null}
934954
{!itemsLoading && items.length === 0 ? <p className="empty">표시할 글이 없습니다.</p> : null}
935955
{items.length > 0 ? (

src/ui/components/ReactionPicker.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Account, ReactionInput } from "../../domain/types";
33
import type { MastodonApi } from "../../services/MastodonApi";
44
import { useClickOutside } from "../hooks/useClickOutside";
55
import { useEmojiManager, type EmojiItem } from "../hooks/useEmojiManager";
6+
import { useToast } from "../state/ToastContext";
67

78
export const ReactionPicker = ({
89
account,
@@ -21,6 +22,8 @@ export const ReactionPicker = ({
2122
const [emojiSearchQuery, setEmojiSearchQuery] = useState("");
2223
const buttonRef = useRef<HTMLButtonElement | null>(null);
2324
const panelRef = useRef<HTMLDivElement | null>(null);
25+
const { showToast } = useToast();
26+
const lastEmojiErrorRef = useRef<string | null>(null);
2427

2528
// useEmojiManager 훅 사용
2629
const {
@@ -37,6 +40,19 @@ export const ReactionPicker = ({
3740
searchEmojis
3841
} = useEmojiManager(account, api, false);
3942

43+
useEffect(() => {
44+
if (emojiStatus !== "error") {
45+
lastEmojiErrorRef.current = null;
46+
return;
47+
}
48+
const message = emojiError ?? "이모지를 불러오지 못했습니다.";
49+
if (message === lastEmojiErrorRef.current) {
50+
return;
51+
}
52+
lastEmojiErrorRef.current = message;
53+
showToast(message, { tone: "error" });
54+
}, [emojiError, emojiStatus, showToast]);
55+
4056
const emojiSearchResults = useMemo(() => {
4157
if (!emojiSearchQuery.trim()) {
4258
return [];

src/ui/components/StatusModal.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Account, CustomEmoji, Status, ThreadContext } from "../../domain/t
33
import type { MastodonApi } from "../../services/MastodonApi";
44
import { TimelineItem } from "./TimelineItem";
55
import BoostIcon from "../assets/boost-icon.svg?react";
6+
import { useToast } from "../state/ToastContext";
67

78
export const StatusModal = ({
89
status,
@@ -120,6 +121,7 @@ export const StatusModal = ({
120121
const [threadContext, setThreadContext] = useState<ThreadContext | null>(null);
121122
const [isLoadingThread, setIsLoadingThread] = useState(false);
122123
const [threadError, setThreadError] = useState<string | null>(null);
124+
const { showToast } = useToast();
123125

124126
// 스레드 컨텍스트 가져오기
125127
useEffect(() => {
@@ -145,6 +147,14 @@ export const StatusModal = ({
145147
fetchThreadContext();
146148
}, [account, api, displayStatus.id]);
147149

150+
useEffect(() => {
151+
if (!threadError) {
152+
return;
153+
}
154+
showToast(threadError, { tone: "error" });
155+
setThreadError(null);
156+
}, [showToast, threadError]);
157+
148158
return (
149159
<div
150160
className="status-modal"
@@ -268,11 +278,6 @@ export const StatusModal = ({
268278

269279
{/* 로딩 상태는 헤더에서 처리 */}
270280

271-
{threadError && (
272-
<div className="thread-error">
273-
<span>{threadError}</span>
274-
</div>
275-
)}
276281
</div>
277282
</div>
278283
</div>

0 commit comments

Comments
 (0)