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
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@

**Learning:** When using a dictionary purely to track the presence of keys (e.g. `has_sent_message[key] = True`), checking for presence with `.get(key, False)` carries unnecessary semantic and memory overhead. Sets in Python provide a cleaner `key in set_name` syntax for boolean presence checks and slightly reduced memory footprint, while maintaining O(1) time complexity.
**Action:** When tracking unique occurrences or boolean presence of items where the value itself doesn't carry additional information, use a `set` and its `.add()` and `in` operators instead of a `dict` mapping to `True` or `False`.
## 2025-02-12 - Memoizing My Tasks view inside JSX
**Learning:** Inline mapping of arrays inside JSX in large React components causes O(N) recalculation on every render. Although the `kanbanBoard` in `TasksLayout` was properly memoized, the `myTasksBoard` list was still mapped inline in the React return block, causing main thread blocking when other layout state (like search) was updated.
**Action:** Always verify that *all* conditional array map rendering blocks are properly wrapped in a `useMemo` hook with all necessary external variables and setters in the dependency array (e.g. `[filteredTicketTasks, setSelectedTaskId, setViewMode]`) to prevent state changes in unrelated components from causing a bottleneck.
Comment on lines +22 to +24

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the memoization invalidation claim.

taskSearch is a direct input to filteredTicketTasks, so search changes invalidate myTasksBoard. Search is not an unrelated update for this board. State that memoization skips work only when filteredTicketTasks and the other dependencies remain unchanged. If the code adds the viewMode guard, include that dependency in the documented example.

Suggested wording change
- ... when other layout state (like search) was updated.
+ ... when unrelated state changes without changing filteredTicketTasks.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## 2025-02-12 - Memoizing My Tasks view inside JSX
**Learning:** Inline mapping of arrays inside JSX in large React components causes O(N) recalculation on every render. Although the `kanbanBoard` in `TasksLayout` was properly memoized, the `myTasksBoard` list was still mapped inline in the React return block, causing main thread blocking when other layout state (like search) was updated.
**Action:** Always verify that *all* conditional array map rendering blocks are properly wrapped in a `useMemo` hook with all necessary external variables and setters in the dependency array (e.g. `[filteredTicketTasks, setSelectedTaskId, setViewMode]`) to prevent state changes in unrelated components from causing a bottleneck.
## 2025-02-12 - Memoizing My Tasks view inside JSX
**Learning:** Inline mapping of arrays inside JSX in large React components causes O(N) recalculation on every render. Although the `kanbanBoard` in `TasksLayout` was properly memoized, the `myTasksBoard` list was still mapped inline in the React return block, causing main thread blocking when unrelated state changes without changing `filteredTicketTasks`.
**Action:** Always verify that *all* conditional array map rendering blocks are properly wrapped in a `useMemo` hook with all necessary external variables and setters in the dependency array (e.g. `[filteredTicketTasks, setSelectedTaskId, setViewMode]`) to prevent state changes in unrelated components from causing a bottleneck.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.jules/bolt.md around lines 22 - 24, Update the “Memoizing My Tasks view
inside JSX” documentation to clarify that memoization skips recalculation only
when filteredTicketTasks and all other dependencies remain unchanged; note that
taskSearch changes filteredTicketTasks and therefore invalidate myTasksBoard. If
the documented example includes a viewMode guard, include viewMode in its
dependency list.

42 changes: 23 additions & 19 deletions frontend/src/components/TasksLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,28 @@ export function TasksLayout() {
[ticketTasks],
);

// ⚡ Bolt: Wrap My Tasks list in useMemo to prevent O(N) re-renders
// 🎯 Why: Mapping over potentially large lists of tasks blocks the main thread during unrelated state updates.
const myTasksBoard = useMemo(() => (
<div className="space-y-4 max-w-4xl mx-auto">
<h2 className="font-bold text-lg mb-4">내 작업</h2>
{filteredTicketTasks.length > 0 ? filteredTicketTasks.map(task => (
<button key={task.id} type="button" className="flex w-full items-center justify-between p-4 rounded-xl border border-border bg-card text-left shadow-sm transition-colors hover:border-primary/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40" onClick={() => { setSelectedTaskId(task.id); setViewMode('작업 상세'); }}>
<div className="flex items-center gap-4">
<div className={`size-3 rounded-full ${task.priority === 'urgent' ? 'bg-red-500' : task.priority === 'high' ? 'bg-orange-500' : 'bg-blue-500'}`}></div>
<div>
<h3 className="font-bold text-sm">{safeTaskTitle(task.title)}</h3>
<p className="text-xs text-muted-foreground mt-1">근거: {getTaskEvidenceLabel(task)} | 원본: {getTaskSourceLabel(task.source_type)}</p>
</div>
</div>
<span className={`px-2 py-1 rounded-full text-xs font-bold ${task.status === 'done' ? 'bg-green-100 text-green-700' : 'bg-secondary text-secondary-foreground'}`}>{taskStatusLabels[task.status]}</span>
</button>
)) : (
<p className="rounded-xl border border-dashed border-border bg-card p-4 text-sm font-semibold text-muted-foreground">서명 세션에 연결된 내 작업이 없습니다.</p>
)}
</div>
), [filteredTicketTasks, setSelectedTaskId, setViewMode]);
Comment on lines +311 to +329

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Guard the memoized board before mapping hidden tasks.

useMemo evaluates this callback when filteredTicketTasks changes, even when viewMode is not '내 작업'. Line 706 only controls whether the already-built element is returned. Search, priority changes, and ticket refreshes therefore allocate an O(N) invisible board.

Guard the callback with viewMode, or isolate the board in a child component rendered only for '내 작업'. Include viewMode in the dependency list when the callback reads it.

Suggested fix
-  const myTasksBoard = useMemo(() => (
+  const myTasksBoard = useMemo(() => {
+    if (viewMode !== '내 작업') return null;
+    return (
     <div className="space-y-4 max-w-4xl mx-auto">
       ...
     </div>
-  ), [filteredTicketTasks, setSelectedTaskId, setViewMode]);
+    );
+  }, [filteredTicketTasks, viewMode, setSelectedTaskId, setViewMode]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const myTasksBoard = useMemo(() => (
<div className="space-y-4 max-w-4xl mx-auto">
<h2 className="font-bold text-lg mb-4">내 작업</h2>
{filteredTicketTasks.length > 0 ? filteredTicketTasks.map(task => (
<button key={task.id} type="button" className="flex w-full items-center justify-between p-4 rounded-xl border border-border bg-card text-left shadow-sm transition-colors hover:border-primary/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40" onClick={() => { setSelectedTaskId(task.id); setViewMode('작업 상세'); }}>
<div className="flex items-center gap-4">
<div className={`size-3 rounded-full ${task.priority === 'urgent' ? 'bg-red-500' : task.priority === 'high' ? 'bg-orange-500' : 'bg-blue-500'}`}></div>
<div>
<h3 className="font-bold text-sm">{safeTaskTitle(task.title)}</h3>
<p className="text-xs text-muted-foreground mt-1">근거: {getTaskEvidenceLabel(task)} | 원본: {getTaskSourceLabel(task.source_type)}</p>
</div>
</div>
<span className={`px-2 py-1 rounded-full text-xs font-bold ${task.status === 'done' ? 'bg-green-100 text-green-700' : 'bg-secondary text-secondary-foreground'}`}>{taskStatusLabels[task.status]}</span>
</button>
)) : (
<p className="rounded-xl border border-dashed border-border bg-card p-4 text-sm font-semibold text-muted-foreground">서명 세션에 연결된 내 작업이 없습니다.</p>
)}
</div>
), [filteredTicketTasks, setSelectedTaskId, setViewMode]);
const myTasksBoard = useMemo(() => {
if (viewMode !== '내 작업') return null;
return (
<div className="space-y-4 max-w-4xl mx-auto">
<h2 className="font-bold text-lg mb-4">내 작업</h2>
{filteredTicketTasks.length > 0 ? filteredTicketTasks.map(task => (
<button key={task.id} type="button" className="flex w-full items-center justify-between p-4 rounded-xl border border-border bg-card text-left shadow-sm transition-colors hover:border-primary/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40" onClick={() => { setSelectedTaskId(task.id); setViewMode('작업 상세'); }}>
<div className="flex items-center gap-4">
<div className={`size-3 rounded-full ${task.priority === 'urgent' ? 'bg-red-500' : task.priority === 'high' ? 'bg-orange-500' : 'bg-blue-500'}`}></div>
<div>
<h3 className="font-bold text-sm">{safeTaskTitle(task.title)}</h3>
<p className="text-xs text-muted-foreground mt-1">근거: {getTaskEvidenceLabel(task)} | 원본: {getTaskSourceLabel(task.source_type)}</p>
</div>
</div>
<span className={`px-2 py-1 rounded-full text-xs font-bold ${task.status === 'done' ? 'bg-green-100 text-green-700' : 'bg-secondary text-secondary-foreground'}`}>{taskStatusLabels[task.status]}</span>
</button>
)) : (
<p className="rounded-xl border border-dashed border-border bg-card p-4 text-sm font-semibold text-muted-foreground">서명 세션에 연결된 내 작업이 없습니다.</p>
)}
</div>
);
}, [filteredTicketTasks, viewMode, setSelectedTaskId, setViewMode]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/TasksLayout.tsx` around lines 311 - 329, Update the
myTasksBoard useMemo callback to return no board or otherwise avoid mapping
filteredTicketTasks when viewMode is not '내 작업'; preserve the existing board
rendering for that mode. Since the callback reads viewMode, include viewMode in
the dependency list, or move the board into a child rendered only for '내 작업'.


// ⚡ Bolt: Wrap Kanban columns in useMemo to prevent O(N) re-renders
// 🎯 Why: Mapping over potentially large lists of tasks by status blocks the main thread during unrelated state updates.
const kanbanBoard = useMemo(() => (
Expand Down Expand Up @@ -681,25 +703,7 @@ export function TasksLayout() {
</section>

{viewMode === '칸반' && kanbanBoard}
{viewMode === '내 작업' && (
<div className="space-y-4 max-w-4xl mx-auto">
<h2 className="font-bold text-lg mb-4">내 작업</h2>
{filteredTicketTasks.length > 0 ? filteredTicketTasks.map(task => (
<button key={task.id} type="button" className="flex w-full items-center justify-between p-4 rounded-xl border border-border bg-card text-left shadow-sm transition-colors hover:border-primary/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40" onClick={() => { setSelectedTaskId(task.id); setViewMode('작업 상세'); }}>
<div className="flex items-center gap-4">
<div className={`size-3 rounded-full ${task.priority === 'urgent' ? 'bg-red-500' : task.priority === 'high' ? 'bg-orange-500' : 'bg-blue-500'}`}></div>
<div>
<h3 className="font-bold text-sm">{safeTaskTitle(task.title)}</h3>
<p className="text-xs text-muted-foreground mt-1">근거: {getTaskEvidenceLabel(task)} | 원본: {getTaskSourceLabel(task.source_type)}</p>
</div>
</div>
<span className={`px-2 py-1 rounded-full text-xs font-bold ${task.status === 'done' ? 'bg-green-100 text-green-700' : 'bg-secondary text-secondary-foreground'}`}>{taskStatusLabels[task.status]}</span>
</button>
)) : (
<p className="rounded-xl border border-dashed border-border bg-card p-4 text-sm font-semibold text-muted-foreground">서명 세션에 연결된 내 작업이 없습니다.</p>
)}
</div>
)}
{viewMode === '내 작업' && myTasksBoard}

{viewMode === '위임한 작업' && (
<div className="space-y-4 max-w-4xl mx-auto">
Expand Down
Loading