Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,9 @@

**Learning:** When generating derived UI state in `useMemo` that joins separate data arrays (like graph edges referencing node IDs), calling helper functions that use `Array.prototype.find()` for every item creates an `O(M * N)` bottleneck.
**Action:** When a loop needs to repeatedly look up related items from another array by ID, pre-compute an `O(N)` `Map` before the loop and use `map.get()` for `O(1)` lookups instead of inline array `.find()` calls.
## 2024-05-24 - [React Component Memoization]
**Learning:** In React components like `WorkspaceHome`, when layout state or polling changes trigger parent re-renders, expensive child components like `EmailDetail` will also re-render unnecessarily if not memoized.
**Action:** Always consider `React.memo` for heavy child components that rely on stable props (like IDs) when the parent component has frequent unrelated state updates.
## 2025-02-12 - Inline Mapping of Arrays in Components
**Learning:** Wrapping inline mapping of arrays within JSX components (e.g. `array.map()`) in a `useMemo` hook is crucial to avoid O(N) re-renders, especially when dealing with lists or UI segments that don't need to change strictly with every state update of the parent.
**Action:** Always wrap `.map()` calls over arrays in components with potentially frequent state updates inside a `useMemo` hook to ensure efficiency and non-blocking performance.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
## [Unreleased]
- EmailDetail 테스트가 지원하지 않는 스레드 병합/분리 버튼을 `textContent`뿐 아니라 `aria-label`과 `title` 접근 가능 이름으로도 검출하도록 바꿔, 아이콘 전용 버튼 회귀를 놓치지 않습니다.
- UUID V4 제너레이터(`uuid_v4_generator`) 도구를 추가하여 런타임에서 범용 고유 식별자 버전 4를 랜덤으로 생성할 수 있게 하였습니다. 테스트 커버리지 100%를 보장합니다.
### 보안 패치 (CodeQL extended current-head)

Expand Down
12 changes: 7 additions & 5 deletions backend/services/text_safety.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,9 +255,7 @@ def _check_html_tag_at(decoded: str, cursor: int) -> tuple[bool, int]:

closing = decoded.find(">", tag_start + 1)
if tag_start < len(decoded) and decoded[tag_start].isalpha():
tag_content = decoded[
tag_start : closing if closing != -1 else None
].strip()
tag_content = decoded[tag_start : closing if closing != -1 else None].strip()
next_cursor = closing + 1 if closing != -1 else len(decoded)

if _looks_like_angle_email(tag_content):
Expand Down Expand Up @@ -340,6 +338,10 @@ def _is_tag_like_segment(value: str) -> bool:
return False
if candidate.startswith("!--") or candidate[0] in {"!", "?"}:
return True

if candidate.startswith("--"):
return False

if candidate[0] == "/":
candidate = candidate[1:].lstrip()
if not candidate or not candidate[0].isalpha():
Expand Down Expand Up @@ -456,12 +458,12 @@ def strip_html_markup(value: str) -> str:
parser.feed(masked)
parser.close()
text = parser.get_text()

cleaned_lines = []
for line in text.splitlines():
cleaned_lines.append(_strip_tag_like_segments(line))
text = "\n".join(cleaned_lines).strip()

for token, original in placeholders.items():
text = text.replace(token, original)
return text
16 changes: 16 additions & 0 deletions frontend/src/components/EmailDetail.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,22 @@ describe("EmailDetail", () => {
expect(container.textContent).toContain("Thread B sibling body");
expect(container.textContent).toContain("2개 메시지");
expect(container.textContent).not.toContain("Thread A stale sibling body");

const unsupportedThreadActions = Array.from(
container.querySelectorAll<HTMLButtonElement>("button"),
).filter((button) => {
const accessibleName = [
button.textContent,
button.getAttribute("aria-label"),
button.getAttribute("title"),
]
.filter((value): value is string => Boolean(value))
.join(" ");
return ["다른 스레드 병합", "스레드 분리"].some((label) =>
accessibleName.includes(label),
);
});
expect(unsupportedThreadActions).toHaveLength(0);
});

it("renders 맥락 종합, action items, and reply drafting in reusable 판단 포인트 cards", async () => {
Expand Down
17 changes: 6 additions & 11 deletions frontend/src/components/EmailDetail.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState, memo } from 'react';
import { apiClient } from '@/lib/api-client';
import { Separator } from "@/components/ui/separator";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
Expand Down Expand Up @@ -102,7 +102,10 @@ function normalizeLlmData(payload: unknown): LlmData {
};
}

export function EmailDetail({ emailId, actionCommand = null }: { emailId: number | null; actionCommand?: EmailDetailActionCommand | null }) {
// ⚡ Bolt: Memoized EmailDetail to prevent unnecessary re-renders
// 🎯 Why: Re-renders of EmailDetail when the parent components (like WorkspaceHome) re-render can cause performance issues, especially when switching active layout tabs or receiving polling updates that don't affect the selected email.
// 📊 Impact: Significantly reduces React reconciliation work when the workspace state changes but the selected email remains the same.
export const EmailDetail = memo(function EmailDetail({ emailId, actionCommand = null }: { emailId: number | null; actionCommand?: EmailDetailActionCommand | null }) {
const [email, setEmail] = useState<EmailData | null>(null);
const [threadEmails, setThreadEmails] = useState<EmailData[]>([]);
const [llmData, setLlmData] = useState<LlmData | null>(null);
Expand Down Expand Up @@ -751,9 +754,6 @@ export function EmailDetail({ emailId, actionCommand = null }: { emailId: number
{conversationMessages.length}개 메시지
</Badge>
</div>
<Button size="sm" variant="outline" className="h-7 text-xs bg-white text-muted-foreground hover:text-foreground">
다른 스레드 병합
</Button>
</div>
<p className="text-xs text-muted-foreground">오래된 메시지부터 최신 메시지 순서로 보여줍니다. 답장은 선택된 메시지를 기준으로 작성됩니다.</p>
{threadLoading && <p role="status" aria-live="polite" className="text-sm text-muted-foreground">대화 흐름을 불러오는 중입니다...</p>}
Expand All @@ -770,11 +770,6 @@ export function EmailDetail({ emailId, actionCommand = null }: { emailId: number
<span className="font-medium text-sm">{toMailDisplayText(msg.sender, '보낸 사람')}</span>
<div className="flex items-center gap-3">
<span className="text-xs text-muted-foreground">{formatEmailDate(msg.date)}</span>
{msg.id !== conversationMessages[0]?.id && (
<Button size="sm" variant="ghost" className="h-6 px-2 text-[10px] text-muted-foreground hover:text-red-600 hover:bg-red-50">
스레드 분리
</Button>
)}
</div>
</div>
{msg.id === email.id && <Badge variant="outline" className="mb-2 border-primary/30 text-[10px] text-primary">선택된 메시지</Badge>}
Expand Down Expand Up @@ -883,4 +878,4 @@ export function EmailDetail({ emailId, actionCommand = null }: { emailId: number
/>
</div>
);
}
});
20 changes: 13 additions & 7 deletions frontend/src/components/calendar/CalendarCandidateView.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import { useMemo } from 'react';
import type { CalendarCandidateEvent } from './types';

type Props = {
visibleCandidateEvents: CalendarCandidateEvent[];
};

export function CalendarCandidateView({ visibleCandidateEvents }: Props) {
// ⚡ Bolt: Wrap candidate events in useMemo to prevent O(N) re-renders when other state changes
const candidateEventList = useMemo(() => (
visibleCandidateEvents.map((event) => (
<article key={event.id} className="rounded-xl border border-border bg-background p-4">
<h4 className="text-sm font-bold">{event.title}</h4>
<p className="mt-2 text-xs text-muted-foreground">{event.source}</p>
<p className="mt-3 rounded-full bg-primary/10 px-3 py-1 text-xs font-bold text-primary">{event.mode}</p>
</article>
))
), [visibleCandidateEvents]);

return (
<section aria-label="일정 후보" className="rounded-2xl border border-border bg-card p-5 shadow-sm">
<h3 className="text-lg font-bold">일정 후보</h3>
<div className="mt-4 grid gap-3 lg:grid-cols-3">
{visibleCandidateEvents.map((event) => (
<article key={event.id} className="rounded-xl border border-border bg-background p-4">
<h4 className="text-sm font-bold">{event.title}</h4>
<p className="mt-2 text-xs text-muted-foreground">{event.source}</p>
<p className="mt-3 rounded-full bg-primary/10 px-3 py-1 text-xs font-bold text-primary">{event.mode}</p>
</article>
))}
{candidateEventList}
{visibleCandidateEvents.length === 0 && (
<p className="rounded-xl border border-border bg-background p-4 text-sm font-bold text-muted-foreground">
표시 중인 캘린더 후보가 없습니다.
Expand Down
20 changes: 13 additions & 7 deletions frontend/src/components/calendar/CalendarWeekView.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import { useMemo } from 'react';
import type { CalendarWeekEvent } from './types';

type Props = {
visibleWeekEvents: CalendarWeekEvent[];
};

export function CalendarWeekView({ visibleWeekEvents }: Props) {
// ⚡ Bolt: Wrap week events in useMemo to prevent O(N) re-renders when other state changes
const weekEventList = useMemo(() => (
visibleWeekEvents.map((event) => (
<article key={event.id} className="rounded-xl border border-border bg-background p-4">
<p className="text-xs font-black text-primary">{event.day}</p>
<h4 className="mt-2 text-sm font-bold">{event.title}</h4>
<p className="mt-2 text-xs font-semibold text-muted-foreground">{event.source}</p>
</article>
))
), [visibleWeekEvents]);

return (
<section aria-label="주간 캘린더" className="rounded-2xl border border-border bg-card p-5 shadow-sm">
<h3 className="text-lg font-bold">주간 캘린더</h3>
<div className="mt-4 grid gap-3 md:grid-cols-5">
{visibleWeekEvents.map((event) => (
<article key={event.id} className="rounded-xl border border-border bg-background p-4">
<p className="text-xs font-black text-primary">{event.day}</p>
<h4 className="mt-2 text-sm font-bold">{event.title}</h4>
<p className="mt-2 text-xs font-semibold text-muted-foreground">{event.source}</p>
</article>
))}
{weekEventList}
{visibleWeekEvents.length === 0 && (
<p className="rounded-xl border border-border bg-background p-4 text-sm font-bold text-muted-foreground">
표시 중인 캘린더 일정이 없습니다.
Expand Down
Loading