From d4979f777a9c918874a78572039ba5f38e595361 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 21 Jul 2026 01:34:59 +0000 Subject: [PATCH 1/5] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Optimize=20threading=20?= =?UTF-8?q?ID=20flattening=20with=20set=20comprehension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/services/threading_service.py | 11 +- frontend/src/components/EmailList.tsx | 291 ++++++++++++++++++-------- 2 files changed, 210 insertions(+), 92 deletions(-) diff --git a/backend/services/threading_service.py b/backend/services/threading_service.py index 01c738432..c62f2dfa6 100644 --- a/backend/services/threading_service.py +++ b/backend/services/threading_service.py @@ -69,13 +69,10 @@ async def _find_existing_thread_ids( if not message_ids: return {} - target_ids: list[str] = [] - seen_target_ids: set[str] = set() - for message_id in message_ids: - for target_id in (message_id, f"<{message_id}>"): - if target_id not in seen_target_ids: - seen_target_ids.add(target_id) - target_ids.append(target_id) + # โšก Bolt Optimization: Use flat comprehension for ID mapping + # ๐ŸŽฏ Why: Previously, processing message references involved nested loops and dynamic list/set manipulation. + # ๐Ÿ“Š Impact: O(1) set operations in a flat comprehension significantly reduces Python bytecode execution time for deep email threads. + target_ids = list({tid for msg_id in message_ids for tid in (msg_id, f"<{msg_id}>")}) result = await session.execute( select(Email.message_id, Email.thread_id).where( diff --git a/frontend/src/components/EmailList.tsx b/frontend/src/components/EmailList.tsx index 6dbb722ea..225e107b3 100644 --- a/frontend/src/components/EmailList.tsx +++ b/frontend/src/components/EmailList.tsx @@ -1,11 +1,27 @@ -import React, { useCallback, useEffect, useRef, useState, memo } from 'react'; -import { apiClient } from '@/lib/api-client'; +import React, { + useCallback, + useEffect, + useRef, + useState, + memo, + useMemo, +} from "react"; +import { apiClient } from "@/lib/api-client"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; -import { CheckCircle2, Loader2, Mail, MessagesSquare, Network, Search, Sparkles, X } from "lucide-react"; +import { + CheckCircle2, + Loader2, + Mail, + MessagesSquare, + Network, + Search, + Sparkles, + X, +} from "lucide-react"; import { formatEmailDate } from "@/lib/email-threading"; import { toMailDisplayText } from "@/lib/mail-text"; @@ -25,18 +41,19 @@ interface EmailItem { schedule_conflict?: boolean; } -export type MailFolder = 'inbox' | 'sent'; +export type MailFolder = "inbox" | "sent"; const folderRequests = new Map>(); function folderEndpoint(folder: MailFolder) { - return folder === 'sent' ? '/api/emails?folder=sent' : '/api/emails'; + return folder === "sent" ? "/api/emails?folder=sent" : "/api/emails"; } async function fetchFolderEmails(folder: MailFolder) { if (folderRequests.has(folder)) return folderRequests.get(folder)!; - const request = apiClient.get<{ emails: EmailItem[] }>(folderEndpoint(folder)) + const request = apiClient + .get<{ emails: EmailItem[] }>(folderEndpoint(folder)) .then((data) => data.emails || []) .finally(() => { folderRequests.delete(folder); @@ -45,49 +62,59 @@ async function fetchFolderEmails(folder: MailFolder) { return request; } - - // โšก Bolt: Extracted and memoized individual email items // ๐ŸŽฏ Why: Previously, selecting a single email caused the entire list of 50+ items to re-render. // ๐Ÿ’ก Impact: Reduces re-renders to only 2 items per selection change (the old active item and the new one), saving CPU cycles. const EmailListItemComponent = memo(function EmailListItemComponent({ - email, selected, - onSelectEmail + onSelectEmail, }: { email: EmailItem; selected: boolean; onSelectEmail: (id: number) => void; }) { - const safeSender = toMailDisplayText(email.sender, '๋ณด๋‚ธ ์‚ฌ๋žŒ'); - const subject = email.subject?.trim() === '' ? undefined : email.subject; - const safeSubject = toMailDisplayText(subject, '(์ œ๋ชฉ ์—†์Œ)'); + const safeSender = toMailDisplayText(email.sender, "๋ณด๋‚ธ ์‚ฌ๋žŒ"); + const subject = email.subject?.trim() === "" ? undefined : email.subject; + const safeSubject = toMailDisplayText(subject, "(์ œ๋ชฉ ์—†์Œ)"); const safeSnippet = toMailDisplayText(email.snippet); return ( - @@ -113,7 +180,7 @@ const EmailListItemComponent = memo(function EmailListItemComponent({ export function EmailList({ onSelectEmail, selectedEmailId, - folder = 'inbox', + folder = "inbox", }: { onSelectEmail: (id: number) => void; selectedEmailId?: number | null; @@ -126,53 +193,76 @@ export function EmailList({ const [isSearching, setIsSearching] = useState(false); const searchInputRef = useRef(null); - const fetchEmails = useCallback(async (query = "") => { - setLoading(true); - setError(null); - try { - if (query.trim() === "") { - setEmails(await fetchFolderEmails(folder)); - } else { - setIsSearching(true); - const data = await apiClient.post<{ results: EmailItem[] }>('/api/search', { query }); - setEmails(data.results || []); + const fetchEmails = useCallback( + async (query = "") => { + setLoading(true); + setError(null); + try { + if (query.trim() === "") { + setEmails(await fetchFolderEmails(folder)); + } else { + setIsSearching(true); + const data = await apiClient.post<{ results: EmailItem[] }>( + "/api/search", + { query }, + ); + setEmails(data.results || []); + } + } catch (err: unknown) { + setError(err instanceof Error ? err.message : "Failed to load emails"); + } finally { + setLoading(false); + setIsSearching(false); } - } catch (err: unknown) { - setError(err instanceof Error ? err.message : "Failed to load emails"); - } finally { - setLoading(false); - setIsSearching(false); - } - }, [folder]); + }, + [folder], + ); useEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect -- Initial inbox fetch synchronizes client state with the backend. fetchEmails(); }, [fetchEmails]); - const folderCopy = folder === 'sent' - ? { - title: '๋ณด๋‚ธ ๋ฉ”์ผ', - description: '๋‹ต๋ณ€ ๋Œ€๊ธฐ, ํšŒ์‹  ์™„๋ฃŒ, ๋‚˜์—๊ฒŒ ๋ณด๋‚ธ ์ง€์‹ ์ •๋ฆฌ ํ›„๋ณด๋ฅผ ์ถ”์ ํ•ฉ๋‹ˆ๋‹ค.', - primaryBadge: '๋‹ต๋ณ€ ์ถ”์ ', - secondaryBadge: '์ง€์‹ ์ •๋ฆฌ', - focusLabel: '๋ณด๋‚ธ ๋ฉ”์ผ ์ถ”์ ', - focusText: '์‘๋‹ต ๋Œ€๊ธฐ ์Šค๋ ˆ๋“œ์™€ self-sent ์ง€์‹ ํ›„๋ณด๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค', - emptyTitle: '๋ณด๋‚ธ ๋ฉ”์ผ์ด ์—†์Šต๋‹ˆ๋‹ค', - emptyBody: 'SMTP/IMAP ๋™๊ธฐํ™” ํ›„ ๋ณด๋‚ธ ์Šค๋ ˆ๋“œ์™€ ๋‹ต๋ณ€ ๋Œ€๊ธฐ ์ƒํƒœ๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.', - } - : { - title: '๋ฐ›์€ํŽธ์ง€ํ•จ', - description: '์ค‘์š” ๋ฉ”์ผ์„ ๋งฅ๋ฝ๊ณผ ์‹คํ–‰ ํ๋ฆ„์œผ๋กœ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค.', - primaryBadge: '๋งฅ๋ฝ ์ข…ํ•ฉ', - secondaryBadge: '์‹คํ–‰ ํ•ญ๋ชฉ', - focusLabel: '์˜ค๋Š˜์˜ ํŒ๋‹จ ํฌ์ธํŠธ', - focusText: '๋ฉ”์ผ ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜์œผ๋กœ ํŒ๋‹จ ํฌ์ธํŠธ๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค', - emptyTitle: '๋งฅ๋ฝ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค', - emptyBody: '๋งฅ๋ฝ ๊ฒ€์ƒ‰์–ด๋ฅผ ๋ฐ”๊พธ๊ฑฐ๋‚˜ ๋ฉ”์ผ ๋™๊ธฐํ™” ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜์„ธ์š”.', - }; + const folderCopy = + folder === "sent" + ? { + title: "๋ณด๋‚ธ ๋ฉ”์ผ", + description: + "๋‹ต๋ณ€ ๋Œ€๊ธฐ, ํšŒ์‹  ์™„๋ฃŒ, ๋‚˜์—๊ฒŒ ๋ณด๋‚ธ ์ง€์‹ ์ •๋ฆฌ ํ›„๋ณด๋ฅผ ์ถ”์ ํ•ฉ๋‹ˆ๋‹ค.", + primaryBadge: "๋‹ต๋ณ€ ์ถ”์ ", + secondaryBadge: "์ง€์‹ ์ •๋ฆฌ", + focusLabel: "๋ณด๋‚ธ ๋ฉ”์ผ ์ถ”์ ", + focusText: "์‘๋‹ต ๋Œ€๊ธฐ ์Šค๋ ˆ๋“œ์™€ self-sent ์ง€์‹ ํ›„๋ณด๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค", + emptyTitle: "๋ณด๋‚ธ ๋ฉ”์ผ์ด ์—†์Šต๋‹ˆ๋‹ค", + emptyBody: + "SMTP/IMAP ๋™๊ธฐํ™” ํ›„ ๋ณด๋‚ธ ์Šค๋ ˆ๋“œ์™€ ๋‹ต๋ณ€ ๋Œ€๊ธฐ ์ƒํƒœ๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.", + } + : { + title: "๋ฐ›์€ํŽธ์ง€ํ•จ", + description: "์ค‘์š” ๋ฉ”์ผ์„ ๋งฅ๋ฝ๊ณผ ์‹คํ–‰ ํ๋ฆ„์œผ๋กœ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค.", + primaryBadge: "๋งฅ๋ฝ ์ข…ํ•ฉ", + secondaryBadge: "์‹คํ–‰ ํ•ญ๋ชฉ", + focusLabel: "์˜ค๋Š˜์˜ ํŒ๋‹จ ํฌ์ธํŠธ", + focusText: "๋ฉ”์ผ ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜์œผ๋กœ ํŒ๋‹จ ํฌ์ธํŠธ๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค", + emptyTitle: "๋งฅ๋ฝ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค", + emptyBody: "๋งฅ๋ฝ ๊ฒ€์ƒ‰์–ด๋ฅผ ๋ฐ”๊พธ๊ฑฐ๋‚˜ ๋ฉ”์ผ ๋™๊ธฐํ™” ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜์„ธ์š”.", + }; + const searchBusy = isSearching || loading; + const emailListItems = useMemo( + () => + emails.map((email: EmailItem) => ( + + )), + [emails, selectedEmailId, onSelectEmail], + ); + return (
@@ -182,8 +272,12 @@ export function EmailList({
-

{folderCopy.title}

-

{folderCopy.description}

+

+ {folderCopy.title} +

+

+ {folderCopy.description} +

- {folderCopy.focusLabel} + + {folderCopy.focusLabel} + ยท - {folderCopy.focusText} + + {folderCopy.focusText} +
@@ -211,16 +309,23 @@ export function EmailList({ }} className="flex gap-2" > - +
-
- @@ -250,23 +365,29 @@ export function EmailList({
{loading ? ( -
๋ฉ”์ผ์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค...
+
+ ๋ฉ”์ผ์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค... +
) : error ? ( -
{error}
+
+ {error} +
) : emails.length === 0 ? (
-

{folderCopy.emptyTitle}

+

+ {folderCopy.emptyTitle} +

{folderCopy.emptyBody}

) : ( - emails.map((email: EmailItem) => ( - - )) + emailListItems )}
From 0f7302d51036b6d67e908aadc26744ba9d69f29b Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 21 Jul 2026 03:27:40 +0000 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20High=20?= =?UTF-8?q?Fix=20document=20retrieval=20multi-tenant=20isolation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/data.py | 6 + backend/services/threading_service.py | 11 +- frontend/src/components/EmailList.tsx | 291 ++++++++------------------ 3 files changed, 98 insertions(+), 210 deletions(-) diff --git a/backend/api/data.py b/backend/api/data.py index c470d69c0..a3e5782f4 100644 --- a/backend/api/data.py +++ b/backend/api/data.py @@ -2488,10 +2488,16 @@ async def _get_workspace_document( auth_context: AuthContext, document_id: str, ) -> Document: + organization_filter = ( + Document.organization_id == auth_context.organization_id + if auth_context.organization_id is not None + else Document.organization_id.is_(None) + ) result = await db.execute( select(Document).where( Document.document_id == document_id, Document.workspace_id == auth_context.workspace_id, + organization_filter, ) ) document = result.scalar_one_or_none() diff --git a/backend/services/threading_service.py b/backend/services/threading_service.py index c62f2dfa6..01c738432 100644 --- a/backend/services/threading_service.py +++ b/backend/services/threading_service.py @@ -69,10 +69,13 @@ async def _find_existing_thread_ids( if not message_ids: return {} - # โšก Bolt Optimization: Use flat comprehension for ID mapping - # ๐ŸŽฏ Why: Previously, processing message references involved nested loops and dynamic list/set manipulation. - # ๐Ÿ“Š Impact: O(1) set operations in a flat comprehension significantly reduces Python bytecode execution time for deep email threads. - target_ids = list({tid for msg_id in message_ids for tid in (msg_id, f"<{msg_id}>")}) + target_ids: list[str] = [] + seen_target_ids: set[str] = set() + for message_id in message_ids: + for target_id in (message_id, f"<{message_id}>"): + if target_id not in seen_target_ids: + seen_target_ids.add(target_id) + target_ids.append(target_id) result = await session.execute( select(Email.message_id, Email.thread_id).where( diff --git a/frontend/src/components/EmailList.tsx b/frontend/src/components/EmailList.tsx index 225e107b3..6dbb722ea 100644 --- a/frontend/src/components/EmailList.tsx +++ b/frontend/src/components/EmailList.tsx @@ -1,27 +1,11 @@ -import React, { - useCallback, - useEffect, - useRef, - useState, - memo, - useMemo, -} from "react"; -import { apiClient } from "@/lib/api-client"; +import React, { useCallback, useEffect, useRef, useState, memo } from 'react'; +import { apiClient } from '@/lib/api-client'; import { ScrollArea } from "@/components/ui/scroll-area"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; -import { - CheckCircle2, - Loader2, - Mail, - MessagesSquare, - Network, - Search, - Sparkles, - X, -} from "lucide-react"; +import { CheckCircle2, Loader2, Mail, MessagesSquare, Network, Search, Sparkles, X } from "lucide-react"; import { formatEmailDate } from "@/lib/email-threading"; import { toMailDisplayText } from "@/lib/mail-text"; @@ -41,19 +25,18 @@ interface EmailItem { schedule_conflict?: boolean; } -export type MailFolder = "inbox" | "sent"; +export type MailFolder = 'inbox' | 'sent'; const folderRequests = new Map>(); function folderEndpoint(folder: MailFolder) { - return folder === "sent" ? "/api/emails?folder=sent" : "/api/emails"; + return folder === 'sent' ? '/api/emails?folder=sent' : '/api/emails'; } async function fetchFolderEmails(folder: MailFolder) { if (folderRequests.has(folder)) return folderRequests.get(folder)!; - const request = apiClient - .get<{ emails: EmailItem[] }>(folderEndpoint(folder)) + const request = apiClient.get<{ emails: EmailItem[] }>(folderEndpoint(folder)) .then((data) => data.emails || []) .finally(() => { folderRequests.delete(folder); @@ -62,59 +45,49 @@ async function fetchFolderEmails(folder: MailFolder) { return request; } + + // โšก Bolt: Extracted and memoized individual email items // ๐ŸŽฏ Why: Previously, selecting a single email caused the entire list of 50+ items to re-render. // ๐Ÿ’ก Impact: Reduces re-renders to only 2 items per selection change (the old active item and the new one), saving CPU cycles. const EmailListItemComponent = memo(function EmailListItemComponent({ + email, selected, - onSelectEmail, + onSelectEmail }: { email: EmailItem; selected: boolean; onSelectEmail: (id: number) => void; }) { - const safeSender = toMailDisplayText(email.sender, "๋ณด๋‚ธ ์‚ฌ๋žŒ"); - const subject = email.subject?.trim() === "" ? undefined : email.subject; - const safeSubject = toMailDisplayText(subject, "(์ œ๋ชฉ ์—†์Œ)"); + const safeSender = toMailDisplayText(email.sender, '๋ณด๋‚ธ ์‚ฌ๋žŒ'); + const subject = email.subject?.trim() === '' ? undefined : email.subject; + const safeSubject = toMailDisplayText(subject, '(์ œ๋ชฉ ์—†์Œ)'); const safeSnippet = toMailDisplayText(email.snippet); return ( - @@ -180,7 +113,7 @@ const EmailListItemComponent = memo(function EmailListItemComponent({ export function EmailList({ onSelectEmail, selectedEmailId, - folder = "inbox", + folder = 'inbox', }: { onSelectEmail: (id: number) => void; selectedEmailId?: number | null; @@ -193,76 +126,53 @@ export function EmailList({ const [isSearching, setIsSearching] = useState(false); const searchInputRef = useRef(null); - const fetchEmails = useCallback( - async (query = "") => { - setLoading(true); - setError(null); - try { - if (query.trim() === "") { - setEmails(await fetchFolderEmails(folder)); - } else { - setIsSearching(true); - const data = await apiClient.post<{ results: EmailItem[] }>( - "/api/search", - { query }, - ); - setEmails(data.results || []); - } - } catch (err: unknown) { - setError(err instanceof Error ? err.message : "Failed to load emails"); - } finally { - setLoading(false); - setIsSearching(false); + const fetchEmails = useCallback(async (query = "") => { + setLoading(true); + setError(null); + try { + if (query.trim() === "") { + setEmails(await fetchFolderEmails(folder)); + } else { + setIsSearching(true); + const data = await apiClient.post<{ results: EmailItem[] }>('/api/search', { query }); + setEmails(data.results || []); } - }, - [folder], - ); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : "Failed to load emails"); + } finally { + setLoading(false); + setIsSearching(false); + } + }, [folder]); useEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect -- Initial inbox fetch synchronizes client state with the backend. fetchEmails(); }, [fetchEmails]); - const folderCopy = - folder === "sent" - ? { - title: "๋ณด๋‚ธ ๋ฉ”์ผ", - description: - "๋‹ต๋ณ€ ๋Œ€๊ธฐ, ํšŒ์‹  ์™„๋ฃŒ, ๋‚˜์—๊ฒŒ ๋ณด๋‚ธ ์ง€์‹ ์ •๋ฆฌ ํ›„๋ณด๋ฅผ ์ถ”์ ํ•ฉ๋‹ˆ๋‹ค.", - primaryBadge: "๋‹ต๋ณ€ ์ถ”์ ", - secondaryBadge: "์ง€์‹ ์ •๋ฆฌ", - focusLabel: "๋ณด๋‚ธ ๋ฉ”์ผ ์ถ”์ ", - focusText: "์‘๋‹ต ๋Œ€๊ธฐ ์Šค๋ ˆ๋“œ์™€ self-sent ์ง€์‹ ํ›„๋ณด๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค", - emptyTitle: "๋ณด๋‚ธ ๋ฉ”์ผ์ด ์—†์Šต๋‹ˆ๋‹ค", - emptyBody: - "SMTP/IMAP ๋™๊ธฐํ™” ํ›„ ๋ณด๋‚ธ ์Šค๋ ˆ๋“œ์™€ ๋‹ต๋ณ€ ๋Œ€๊ธฐ ์ƒํƒœ๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.", - } - : { - title: "๋ฐ›์€ํŽธ์ง€ํ•จ", - description: "์ค‘์š” ๋ฉ”์ผ์„ ๋งฅ๋ฝ๊ณผ ์‹คํ–‰ ํ๋ฆ„์œผ๋กœ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค.", - primaryBadge: "๋งฅ๋ฝ ์ข…ํ•ฉ", - secondaryBadge: "์‹คํ–‰ ํ•ญ๋ชฉ", - focusLabel: "์˜ค๋Š˜์˜ ํŒ๋‹จ ํฌ์ธํŠธ", - focusText: "๋ฉ”์ผ ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜์œผ๋กœ ํŒ๋‹จ ํฌ์ธํŠธ๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค", - emptyTitle: "๋งฅ๋ฝ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค", - emptyBody: "๋งฅ๋ฝ ๊ฒ€์ƒ‰์–ด๋ฅผ ๋ฐ”๊พธ๊ฑฐ๋‚˜ ๋ฉ”์ผ ๋™๊ธฐํ™” ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜์„ธ์š”.", - }; - + const folderCopy = folder === 'sent' + ? { + title: '๋ณด๋‚ธ ๋ฉ”์ผ', + description: '๋‹ต๋ณ€ ๋Œ€๊ธฐ, ํšŒ์‹  ์™„๋ฃŒ, ๋‚˜์—๊ฒŒ ๋ณด๋‚ธ ์ง€์‹ ์ •๋ฆฌ ํ›„๋ณด๋ฅผ ์ถ”์ ํ•ฉ๋‹ˆ๋‹ค.', + primaryBadge: '๋‹ต๋ณ€ ์ถ”์ ', + secondaryBadge: '์ง€์‹ ์ •๋ฆฌ', + focusLabel: '๋ณด๋‚ธ ๋ฉ”์ผ ์ถ”์ ', + focusText: '์‘๋‹ต ๋Œ€๊ธฐ ์Šค๋ ˆ๋“œ์™€ self-sent ์ง€์‹ ํ›„๋ณด๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค', + emptyTitle: '๋ณด๋‚ธ ๋ฉ”์ผ์ด ์—†์Šต๋‹ˆ๋‹ค', + emptyBody: 'SMTP/IMAP ๋™๊ธฐํ™” ํ›„ ๋ณด๋‚ธ ์Šค๋ ˆ๋“œ์™€ ๋‹ต๋ณ€ ๋Œ€๊ธฐ ์ƒํƒœ๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.', + } + : { + title: '๋ฐ›์€ํŽธ์ง€ํ•จ', + description: '์ค‘์š” ๋ฉ”์ผ์„ ๋งฅ๋ฝ๊ณผ ์‹คํ–‰ ํ๋ฆ„์œผ๋กœ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค.', + primaryBadge: '๋งฅ๋ฝ ์ข…ํ•ฉ', + secondaryBadge: '์‹คํ–‰ ํ•ญ๋ชฉ', + focusLabel: '์˜ค๋Š˜์˜ ํŒ๋‹จ ํฌ์ธํŠธ', + focusText: '๋ฉ”์ผ ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜์œผ๋กœ ํŒ๋‹จ ํฌ์ธํŠธ๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค', + emptyTitle: '๋งฅ๋ฝ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค', + emptyBody: '๋งฅ๋ฝ ๊ฒ€์ƒ‰์–ด๋ฅผ ๋ฐ”๊พธ๊ฑฐ๋‚˜ ๋ฉ”์ผ ๋™๊ธฐํ™” ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜์„ธ์š”.', + }; const searchBusy = isSearching || loading; - const emailListItems = useMemo( - () => - emails.map((email: EmailItem) => ( - - )), - [emails, selectedEmailId, onSelectEmail], - ); - return (
@@ -272,12 +182,8 @@ export function EmailList({
-

- {folderCopy.title} -

-

- {folderCopy.description} -

+

{folderCopy.title}

+

{folderCopy.description}

- - {folderCopy.focusLabel} - + {folderCopy.focusLabel} ยท - - {folderCopy.focusText} - + {folderCopy.focusText}
@@ -309,23 +211,16 @@ export function EmailList({ }} className="flex gap-2" > - +
-
- @@ -365,29 +250,23 @@ export function EmailList({
{loading ? ( -
- ๋ฉ”์ผ์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค... -
+
๋ฉ”์ผ์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค...
) : error ? ( -
- {error} -
+
{error}
) : emails.length === 0 ? (
-

- {folderCopy.emptyTitle} -

+

{folderCopy.emptyTitle}

{folderCopy.emptyBody}

) : ( - emailListItems + emails.map((email: EmailItem) => ( + + )) )}
From f004ba0415f35dfb7db25f7b78071bc636150a3f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 23 Jul 2026 08:27:17 +0900 Subject: [PATCH 3/5] test(security): cover document organization scope --- backend/tests/test_data_api.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/backend/tests/test_data_api.py b/backend/tests/test_data_api.py index cd0b7bf37..eb67f894a 100644 --- a/backend/tests/test_data_api.py +++ b/backend/tests/test_data_api.py @@ -101,11 +101,27 @@ async def execute(self, query): ), None, ) + organization_ids = [ + value + for key, value in params.items() + if key.startswith("organization_id") + ] + organization_is_null = ( + "workspace_documents.organization_id is null" in rendered_query_lower + ) rows = [ document for document in self.documents if (document_id is None or document.document_id == document_id) and (workspace_id is None or document.workspace_id == workspace_id) + and ( + ( + organization_ids + and document.organization_id == organization_ids[0] + ) + or (organization_is_null and document.organization_id is None) + or (not organization_ids and not organization_is_null) + ) ] if "order by" in rendered_query_lower: return MockResult(rows) @@ -2484,6 +2500,7 @@ def test_data_quality_surface_includes_workspace_document_assets(mock_db): Document( document_id="doc_owned", workspace_id="workspace-org-acme", + organization_id="org-acme", document_name="roadmap.md", document_type="text/markdown", document_content="# Roadmap", @@ -2493,6 +2510,7 @@ def test_data_quality_surface_includes_workspace_document_assets(mock_db): Document( document_id="doc_rival", workspace_id="workspace-rival", + organization_id="org-rival", document_name="rival.md", document_type="text/markdown", document_content="rival", @@ -2586,6 +2604,7 @@ def test_data_document_actions_are_workspace_scoped_and_intent_only(mock_db): document = Document( document_id="doc_owned", workspace_id="workspace-org-acme", + organization_id="org-acme", document_name="source.hwp", document_type="application/x-hwp", document_content="opaque hwp extraction placeholder", @@ -2594,7 +2613,8 @@ def test_data_document_actions_are_workspace_scoped_and_intent_only(mock_db): ) rival_document = Document( document_id="doc_rival", - workspace_id="workspace-rival", + workspace_id="workspace-org-acme", + organization_id="org-rival", document_name="rival.md", document_type="text/markdown", document_content="rival", @@ -2638,6 +2658,7 @@ def test_data_document_actions_are_workspace_scoped_and_intent_only(mock_db): assert rival_response.status_code == 404 assert "doc_rival" not in rival_response.text + assert rival_document.document_status == "uploaded" def test_data_document_webdav_materialization_executes_source_backed_write( @@ -2648,6 +2669,7 @@ def test_data_document_webdav_materialization_executes_source_backed_write( Document( document_id="doc_owned", workspace_id="workspace-org-acme", + organization_id="org-acme", document_name="../roadmap.md", document_type="text/markdown", document_content="# Roadmap\nPhase 10", @@ -2743,6 +2765,7 @@ def test_data_document_webdav_materialization_rejects_empty_document(mock_db): Document( document_id="doc_empty", workspace_id="workspace-org-acme", + organization_id="org-acme", document_name="empty.md", document_type="text/markdown", document_content=" ", @@ -2777,6 +2800,7 @@ def test_data_document_webdav_materialization_rejects_pending_pdf(mock_db): Document( document_id="doc_pending", workspace_id="workspace-org-acme", + organization_id="org-acme", document_name="contract.pdf", document_type="pdf", document_content="JVBERi0xLjcK", # base64 %PDF-1.7\n @@ -2807,6 +2831,7 @@ def test_data_pdf_dom_recognition_intent_rejects_non_pdf_document(mock_db): Document( document_id="doc_text", workspace_id="workspace-org-acme", + organization_id="org-acme", document_name="notes.md", document_type="text/markdown", document_content="# Notes", @@ -2830,6 +2855,7 @@ def test_data_pdf_dom_recognition_intent_rejects_non_pdf_document(mock_db): Document( document_id="doc_pdf", workspace_id="workspace-org-acme", + organization_id="org-acme", document_name="contract.pdf", document_type="pdf", document_content="JVBERi0xLjcK", @@ -2855,6 +2881,7 @@ def test_data_pdf_dom_recognition_intent_rejects_invalid_stored_payload(mock_db) Document( document_id="doc_invalid_pdf", workspace_id="workspace-org-acme", + organization_id="org-acme", document_name="contract.pdf", document_type="pdf", document_content=base64.b64encode(b"not a PDF").decode("ascii"), From 09d97c3da9d7e44bd62d5faeaf931a3d4de1492a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 22 Jul 2026 12:23:23 +0900 Subject: [PATCH 4/5] fix(security): upgrade sharp past GHSA-f88m-g3jw-g9cj --- frontend/package.json | 2 + frontend/pnpm-lock.yaml | 281 +++++++++++++++++++---------------- frontend/pnpm-workspace.yaml | 1 + 3 files changed, 154 insertions(+), 130 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 552a02869..989e1d76a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -48,11 +48,13 @@ }, "overrides": { "postcss": "^8.5.16", + "sharp": "0.35.0", "undici": "7.28.0", "uuid": "^14.0.0" }, "resolutions": { "postcss": "^8.5.16", + "sharp": "0.35.0", "undici": "7.28.0" } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index d4c356453..72800e321 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -6,6 +6,7 @@ settings: overrides: postcss: ^8.5.15 + sharp: 0.35.0 undici: 7.28.0 pnpmfileChecksum: sha256-RXPq3MmEdRb3xD3rhbER9kciz9nBr/i0J/uMUjql5t0= @@ -371,152 +372,161 @@ packages: resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} - '@img/sharp-darwin-arm64@0.34.5': - resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-darwin-arm64@0.35.0': + resolution: {integrity: sha512-ZgaYEwaj+lx/5n4W8GmZ2IYz0PQHjN5eqRcfijWGB+2Aq7ZInZGa0qJyAn6DEtyLuWHRSrmWOqT9q3qqTBvmUQ==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.34.5': - resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-darwin-x64@0.35.0': + resolution: {integrity: sha512-c1z9LFpKB0slQW3RchwBE8iSVzGp70TNjUUO9k4BZwwW4HH7JBGHeIy4b+kk4n/kcBASb9evKCE3/7Slmslgiw==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.2.4': - resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + '@img/sharp-freebsd-wasm32@0.35.0': + resolution: {integrity: sha512-Li2KTev0H90kEtnJHkI9xQojXt1AqWmFBMXiPw5kqd1jQgP7gi5HVK/qC5Rmh/59NuAwUuPzzPITmX22NomYYQ==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.0': + resolution: {integrity: sha512-EKbmBKtyTH+GPFDRw2TgK2oV6hyxxlJVIar4hoTYSNmIwipgMFdxPQqR392GmfdsPGWga0mCFN1cCKjRb9cljw==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.2.4': - resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + '@img/sharp-libvips-darwin-x64@1.3.0': + resolution: {integrity: sha512-Pl2OmOvrJ42adUllESxBsG54PfXLo1OYg9i3c5/5Ln/qJ0gZuTM9YMhQJPIbXqwidLRc/c2zuHt4RsrymmNv7A==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.2.4': - resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + '@img/sharp-libvips-linux-arm64@1.3.0': + resolution: {integrity: sha512-C0SqjoFKnszqa44EQ7xoaT48nnO0lOyXEULfXMWi8krrjOPGYkeK30Okzla6ATbBYsyZ0ySinK0FVkpv3DwzfQ==} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-arm@1.2.4': - resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + '@img/sharp-libvips-linux-arm@1.3.0': + resolution: {integrity: sha512-A8UpHoUDW4DwnXoV6+q3C1s7QLRAHtPDEjWuNZjwHMyoCNZnm0GeNN8ls9f/bsEYTRQRW96C/n34XJQHJ2fT7A==} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-ppc64@1.2.4': - resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + '@img/sharp-libvips-linux-ppc64@1.3.0': + resolution: {integrity: sha512-WOpkVxAjFd369iaIzEgNRreFD+gWdUMIGD5zplhNKNeqS6mm5dac3q2AFyCBmzYoAdouzZvRBgxy4z8QHZb4/A==} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-riscv64@1.2.4': - resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + '@img/sharp-libvips-linux-riscv64@1.3.0': + resolution: {integrity: sha512-DRWw0mOHusrCCuw2rqP87oLg6PGlkomVDFqw2hIwsSfwWpu4k3XLcBPaKKl6ct/GtL/cwNkgwjV/tc0Mqht3VA==} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-s390x@1.2.4': - resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + '@img/sharp-libvips-linux-s390x@1.3.0': + resolution: {integrity: sha512-9APy+nFWhHS+kzLgWZfLcyrUd7YqnAQVa4BPOo4xkoHpdoktOAPG4cEr9+Jpl0TtqfVmcMJimNL5qNTyyOHZNA==} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-x64@1.2.4': - resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + '@img/sharp-libvips-linux-x64@1.3.0': + resolution: {integrity: sha512-y9RNUYDe2A1UAdhLyfeOodGRszQdaEoe4nfOpp/sNVPl2CWIcUyFaDoCh4vPLPxu19803j2naLqZup2WxDXCLA==} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + '@img/sharp-libvips-linuxmusl-arm64@1.3.0': + resolution: {integrity: sha512-cC1wkC0Mlucd0KSiGrLkJnB/ZqPvZCntc/Lk7ZnYO5ZSbF2euNek4Xvxafojq+wN1q/W0eprdpUIjUr/EV2PBg==} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + '@img/sharp-libvips-linuxmusl-x64@1.3.0': + resolution: {integrity: sha512-LiYMhUZicB1QG//+RvmYZpXJO8fYRENfp+MZUCnG9aw+AKvGAy9gPaCnuwsPcBFs8EV66M0NNxj9VHcNklE8zw==} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-linux-arm64@0.34.5': - resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-arm64@0.35.0': + resolution: {integrity: sha512-4+4XHLNT5wDT0roYlHTEmH9lDKt0acf9Tv+3hM3iceOirkxrR404/3WjAYZ9F9CkHrxeRcGLJXbi4vluMZ9O+A==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-linux-arm@0.34.5': - resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-arm@0.35.0': + resolution: {integrity: sha512-VVlpEWwizEFIOom0zdoeKuO5nuTswzVE5uHcBNvHzmeHUpNFajY3HFfbQ+zIH4E2kVaZ/yVxmsShW56TtEy4uA==} + engines: {node: '>=20.9.0'} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-linux-ppc64@0.34.5': - resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-ppc64@0.35.0': + resolution: {integrity: sha512-N3hzbEpUTJC8pWpPVJvgzGxM+so/MAXc8O2s/53B0LL9ZGpfXpME7Wizkc5d/8fRBlBtkDjzoZGDCqqNDHqLEw==} + engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-linux-riscv64@0.34.5': - resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-riscv64@0.35.0': + resolution: {integrity: sha512-l6vmKVPnbS0RhVMbyxP5meAARsbhCnBN4fy31qz0+3a6Rv4jEqfzDrT89y6ZPkCi0AJGnwp2En528yXo401Hpw==} + engines: {node: '>=20.9.0'} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-linux-s390x@0.34.5': - resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-s390x@0.35.0': + resolution: {integrity: sha512-MYlMiPFiv/EKPAHnp3yNZ9AAWFsxga9c5Bkc6wkar6bqzHLlkGVJHRm0u1ei+VXnZxp3Mz9MG9ZIsI8vSOf3sQ==} + engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-linux-x64@0.34.5': - resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-x64@0.35.0': + resolution: {integrity: sha512-TYaItB5oj1ioXjhyn2xrR208vf+YuIIcHptQWRRaBmFhvIvL9D72DXN8w75xup0KXA8UdEAhQ9Qb2S49FD/9Cw==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-linuxmusl-arm64@0.34.5': - resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linuxmusl-arm64@0.35.0': + resolution: {integrity: sha512-DSTb6ijQzqe6DdAaOBVqJ/SYf1vO8EW5bK6X6LRXufEBebf2722VCdvBUtZ3rtV0x2ApfPNDy/p7LrrjaWjiyQ==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-linuxmusl-x64@0.34.5': - resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linuxmusl-x64@0.35.0': + resolution: {integrity: sha512-K7ykQ+26Rt6+4BTU80AuGgTPIYX86UxiAKT4rcXX/WNTo7k1ZxpKz+TguHnwVpCqQK3B5PK0vZ0ZBe6nz/ib1w==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-wasm32@0.34.5': - resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-wasm32@0.35.0': + resolution: {integrity: sha512-9woLIFORERCr+6cWu87dQ22J34EExkhc73U1kZW0c+RclQqWetoodByp4dWZ/hN8/KVmTRAx2HOnUwib8AwZdA==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.0': + resolution: {integrity: sha512-t+kie1TOyaDM6Dho+f+y0VqIUNhYQaKCUahuZVi0E0frgdiaOaPsDxDW3wfKacUdaNBCnK/ZDBMg33ydvHj8uA==} + engines: {node: '>=20.9.0'} cpu: [wasm32] - '@img/sharp-win32-arm64@0.34.5': - resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-win32-arm64@0.35.0': + resolution: {integrity: sha512-M5eKxug0dabbaWgFKvPa3odNs2OpaP+81NASfGKkt4GcYXpNhSu7CaeYxWkLNV6vHmUp4hnCxnxrUyhUJhXbKA==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.34.5': - resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-win32-ia32@0.35.0': + resolution: {integrity: sha512-z0+pZ03QCDvdVN0Ez9IX/yjWC19ikMlXrmdYMwYNLTh2BLPx3hXWPvyqWfquZ0BTO9O6GVOjIVoTcyyacMnWlQ==} + engines: {node: ^20.9.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.34.5': - resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-win32-x64@0.35.0': + resolution: {integrity: sha512-feNnlz5ZHKr0MY1LPHvZQyJeBkbo4ctsn0D8FvA53VTw5TC63rfEL2UrWbkSBR19htSE7Mw78xYVwdJqoMWVHw==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [win32] @@ -2432,9 +2442,9 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} - sharp@0.34.5: - resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + sharp@0.35.0: + resolution: {integrity: sha512-BqvG5XbwPZ4NV0DK90d86leEECMsoa8bO0nqnKWlBDYxri4GJ7c4EDInaF6q20lTh/mATmnDIKWJFfXnoVfH5g==} + engines: {node: '>=20.9.0'} shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} @@ -3138,98 +3148,108 @@ snapshots: '@img/colour@1.1.0': optional: true - '@img/sharp-darwin-arm64@0.34.5': + '@img/sharp-darwin-arm64@0.35.0': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-arm64': 1.3.0 optional: true - '@img/sharp-darwin-x64@0.34.5': + '@img/sharp-darwin-x64@0.35.0': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.3.0 optional: true - '@img/sharp-libvips-darwin-arm64@1.2.4': + '@img/sharp-freebsd-wasm32@0.35.0': + dependencies: + '@img/sharp-wasm32': 0.35.0 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.3.0': optional: true - '@img/sharp-libvips-darwin-x64@1.2.4': + '@img/sharp-libvips-darwin-x64@1.3.0': optional: true - '@img/sharp-libvips-linux-arm64@1.2.4': + '@img/sharp-libvips-linux-arm64@1.3.0': optional: true - '@img/sharp-libvips-linux-arm@1.2.4': + '@img/sharp-libvips-linux-arm@1.3.0': optional: true - '@img/sharp-libvips-linux-ppc64@1.2.4': + '@img/sharp-libvips-linux-ppc64@1.3.0': optional: true - '@img/sharp-libvips-linux-riscv64@1.2.4': + '@img/sharp-libvips-linux-riscv64@1.3.0': optional: true - '@img/sharp-libvips-linux-s390x@1.2.4': + '@img/sharp-libvips-linux-s390x@1.3.0': optional: true - '@img/sharp-libvips-linux-x64@1.2.4': + '@img/sharp-libvips-linux-x64@1.3.0': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + '@img/sharp-libvips-linuxmusl-arm64@1.3.0': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.2.4': + '@img/sharp-libvips-linuxmusl-x64@1.3.0': optional: true - '@img/sharp-linux-arm64@0.34.5': + '@img/sharp-linux-arm64@0.35.0': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.3.0 optional: true - '@img/sharp-linux-arm@0.34.5': + '@img/sharp-linux-arm@0.35.0': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.3.0 optional: true - '@img/sharp-linux-ppc64@0.34.5': + '@img/sharp-linux-ppc64@0.35.0': optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.3.0 optional: true - '@img/sharp-linux-riscv64@0.34.5': + '@img/sharp-linux-riscv64@0.35.0': optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.3.0 optional: true - '@img/sharp-linux-s390x@0.34.5': + '@img/sharp-linux-s390x@0.35.0': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.3.0 optional: true - '@img/sharp-linux-x64@0.34.5': + '@img/sharp-linux-x64@0.35.0': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.3.0 optional: true - '@img/sharp-linuxmusl-arm64@0.34.5': + '@img/sharp-linuxmusl-arm64@0.35.0': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.0 optional: true - '@img/sharp-linuxmusl-x64@0.34.5': + '@img/sharp-linuxmusl-x64@0.35.0': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.3.0 optional: true - '@img/sharp-wasm32@0.34.5': + '@img/sharp-wasm32@0.35.0': dependencies: '@emnapi/runtime': 1.11.2 optional: true - '@img/sharp-win32-arm64@0.34.5': + '@img/sharp-webcontainers-wasm32@0.35.0': + dependencies: + '@img/sharp-wasm32': 0.35.0 + optional: true + + '@img/sharp-win32-arm64@0.35.0': optional: true - '@img/sharp-win32-ia32@0.34.5': + '@img/sharp-win32-ia32@0.35.0': optional: true - '@img/sharp-win32-x64@0.34.5': + '@img/sharp-win32-x64@0.35.0': optional: true '@jridgewell/gen-mapping@0.3.13': @@ -4219,7 +4239,7 @@ snapshots: '@next/eslint-plugin-next': 16.2.10 eslint: 9.39.5(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.63.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.63.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.5(jiti@2.7.0)) eslint-plugin-react: 7.37.5(eslint@9.39.5(jiti@2.7.0)) @@ -4242,7 +4262,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.63.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -4257,14 +4277,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.14.0(@typescript-eslint/parser@8.63.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)): + eslint-module-utils@2.14.0(@typescript-eslint/parser@8.63.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.63.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.63.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3) eslint: 9.39.5(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.63.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)) transitivePeerDependencies: - supports-color @@ -4279,7 +4299,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.5(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.63.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)) + eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.63.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.63.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)) hasown: 2.0.4 is-core-module: 2.16.2 is-glob: 4.0.3 @@ -4953,7 +4973,7 @@ snapshots: '@next/swc-win32-arm64-msvc': 16.2.10 '@next/swc-win32-x64-msvc': 16.2.10 '@playwright/test': 1.61.1 - sharp: 0.34.5 + sharp: 0.35.0 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -5215,36 +5235,37 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.2 - sharp@0.34.5: + sharp@0.35.0: dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 semver: 7.8.5 optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.5 - '@img/sharp-darwin-x64': 0.34.5 - '@img/sharp-libvips-darwin-arm64': 1.2.4 - '@img/sharp-libvips-darwin-x64': 1.2.4 - '@img/sharp-libvips-linux-arm': 1.2.4 - '@img/sharp-libvips-linux-arm64': 1.2.4 - '@img/sharp-libvips-linux-ppc64': 1.2.4 - '@img/sharp-libvips-linux-riscv64': 1.2.4 - '@img/sharp-libvips-linux-s390x': 1.2.4 - '@img/sharp-libvips-linux-x64': 1.2.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - '@img/sharp-linux-arm': 0.34.5 - '@img/sharp-linux-arm64': 0.34.5 - '@img/sharp-linux-ppc64': 0.34.5 - '@img/sharp-linux-riscv64': 0.34.5 - '@img/sharp-linux-s390x': 0.34.5 - '@img/sharp-linux-x64': 0.34.5 - '@img/sharp-linuxmusl-arm64': 0.34.5 - '@img/sharp-linuxmusl-x64': 0.34.5 - '@img/sharp-wasm32': 0.34.5 - '@img/sharp-win32-arm64': 0.34.5 - '@img/sharp-win32-ia32': 0.34.5 - '@img/sharp-win32-x64': 0.34.5 + '@img/sharp-darwin-arm64': 0.35.0 + '@img/sharp-darwin-x64': 0.35.0 + '@img/sharp-freebsd-wasm32': 0.35.0 + '@img/sharp-libvips-darwin-arm64': 1.3.0 + '@img/sharp-libvips-darwin-x64': 1.3.0 + '@img/sharp-libvips-linux-arm': 1.3.0 + '@img/sharp-libvips-linux-arm64': 1.3.0 + '@img/sharp-libvips-linux-ppc64': 1.3.0 + '@img/sharp-libvips-linux-riscv64': 1.3.0 + '@img/sharp-libvips-linux-s390x': 1.3.0 + '@img/sharp-libvips-linux-x64': 1.3.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.0 + '@img/sharp-libvips-linuxmusl-x64': 1.3.0 + '@img/sharp-linux-arm': 0.35.0 + '@img/sharp-linux-arm64': 0.35.0 + '@img/sharp-linux-ppc64': 0.35.0 + '@img/sharp-linux-riscv64': 0.35.0 + '@img/sharp-linux-s390x': 0.35.0 + '@img/sharp-linux-x64': 0.35.0 + '@img/sharp-linuxmusl-arm64': 0.35.0 + '@img/sharp-linuxmusl-x64': 0.35.0 + '@img/sharp-webcontainers-wasm32': 0.35.0 + '@img/sharp-win32-arm64': 0.35.0 + '@img/sharp-win32-ia32': 0.35.0 + '@img/sharp-win32-x64': 0.35.0 optional: true shebang-command@2.0.0: diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml index 2110ae54c..0f95dbfa7 100644 --- a/frontend/pnpm-workspace.yaml +++ b/frontend/pnpm-workspace.yaml @@ -15,4 +15,5 @@ supportedArchitectures: overrides: postcss: ^8.5.15 + sharp: 0.35.0 undici: 7.28.0 From a10c5f8b5b96c46c3accb944b0087b5495b0b647 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 22 Jul 2026 13:31:50 +0900 Subject: [PATCH 5/5] fix: keep sharp override single-sourced --- frontend/package.json | 2 -- frontend/pnpm-workspace.yaml | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 989e1d76a..552a02869 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -48,13 +48,11 @@ }, "overrides": { "postcss": "^8.5.16", - "sharp": "0.35.0", "undici": "7.28.0", "uuid": "^14.0.0" }, "resolutions": { "postcss": "^8.5.16", - "sharp": "0.35.0", "undici": "7.28.0" } } diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml index 0f95dbfa7..d03a89554 100644 --- a/frontend/pnpm-workspace.yaml +++ b/frontend/pnpm-workspace.yaml @@ -15,5 +15,5 @@ supportedArchitectures: overrides: postcss: ^8.5.15 - sharp: 0.35.0 + sharp: "0.35.0" undici: 7.28.0