[FEAT] 집중 모드(Focus) 타이머·태스크 상세 화면 구현#116
Conversation
- Figma 디자인을 기반으로 focus 페이지에 타이머(재생/일시정지/시간추가) 영역을 구현했습니다
- Figma 디자인 대조 결과 재생 중 상태에서는 재생 버튼뿐 아니라 종료·시간추가 버튼도 함께 강조되어야 함을 확인하여 수정했습니다
- w-[546px] arbitrary value를 동일한 값의 스페이싱 스케일 단위(w-136.5)로 변경했습니다
…to feat/web/109-focus-view # Conflicts: # apps/timo-web/app/[locale]/(main)/focus/page.tsx
- Figma 디자인을 기반으로 태스크 상세 영역(날짜, 체크박스, 하위 테스크, 메모, 재생 버튼)을 구현했습니다 - FocusTaskContainer와 FocusTimerContainer를 FocusSessionContainer로 통합해 재생/완료 상태를 공유하도록 했습니다
- 재생 중에도 종료·시간추가 버튼은 회색을 유지하고 재생 버튼만 강조되도록 이전 변경을 되돌렸습니다
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 22 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
WalkthroughFocus 페이지에 세션 화면이 추가되었습니다. 태스크 타입, 목데이터, 날짜·시간 포맷 유틸, 카드 UI, 상태 컨테이너가 새로 연결되고, 페이지는 새 컨테이너를 렌더링합니다. ChangesFocus 세션 UI 구현
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant FocusTaskCard
participant FocusSessionContainer
participant TimerControls
User->>FocusTaskCard: 완료/재생/서브태스크 체크
FocusTaskCard->>FocusSessionContainer: 상태 토글 콜백 호출
FocusSessionContainer->>FocusSessionContainer: task 상태 갱신
FocusSessionContainer->>TimerControls: 종료/재생/시간추가 콜백 전달
FocusSessionContainer->>FocusTaskCard: 갱신된 task 다시 전달
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Timo Performance ReportBundle Size — timo-web
Lighthouse — timo-web
Image Optimization — timo-web
측정 커밋: |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with 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.
Inline comments:
In `@apps/timo-web/app/`[locale]/(main)/focus/_components/FocusTaskCard.tsx:
- Around line 42-112: The top-level wrapper in FocusTaskCard should use a
semantic container instead of a plain div to better represent this as a task
card. Replace the outermost container in FocusTaskCard with an appropriate
semantic tag such as article or section, keeping the existing layout and styling
classes intact. Make sure the change is applied only to the card root so the
inner structure and handlers remain unchanged.
In
`@apps/timo-web/app/`[locale]/(main)/focus/_containers/FocusSessionContainer.tsx:
- Line 42: The handleAddTime placeholder in FocusSessionContainer is left as an
empty function, unlike the other handlers that clearly mark pending work with a
TODO comment. Update handleAddTime to match the existing handler pattern in
FocusSessionContainer by adding an explicit TODO marker (or otherwise making the
intentional stub obvious) so it is clear this is unfinished work rather than an
accidental no-op.
- Around line 67-69: The top-level wrapper in FocusSessionContainer should use a
semantic main landmark instead of a plain div. Update the outer container
returned by FocusSessionContainer so the page’s primary content is wrapped with
main while preserving the existing layout classes and nested structure. Keep the
change localized to the FocusSessionContainer component and ensure the inner
flex container remains unchanged.
In `@apps/timo-web/app/`[locale]/(main)/focus/_types/task-type.ts:
- Line 4: The boolean field name in the Task type does not follow the
is/has/should/can naming convention. Rename the `completed` property in `Task`
to `isCompleted`, then update every usage that reads or writes this field in
`task-mock.ts` and `FocusTaskCard.tsx` so the type and consumers stay aligned
with the new name.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: aa5d5197-78ff-4b30-af3a-ff1e1048ae30
📒 Files selected for processing (7)
apps/timo-web/app/[locale]/(main)/focus/_components/FocusTaskCard.tsxapps/timo-web/app/[locale]/(main)/focus/_containers/FocusSessionContainer.tsxapps/timo-web/app/[locale]/(main)/focus/_mocks/task-mock.tsapps/timo-web/app/[locale]/(main)/focus/_types/task-type.tsapps/timo-web/app/[locale]/(main)/focus/_utils/date.tsapps/timo-web/app/[locale]/(main)/focus/_utils/duration.tsapps/timo-web/app/[locale]/(main)/focus/page.tsx
| <div className="flex w-full min-w-0 flex-col items-start gap-5 px-[34.5px]"> | ||
| <div className="text-timo-gray-900 flex flex-col items-start"> | ||
| <p className="typo-headline-b-30">{dayNumber}</p> | ||
| <p className="typo-headline-m-14">{dayOfWeek}</p> | ||
| </div> | ||
|
|
||
| <div className="flex w-full items-center gap-2.5"> | ||
| <Checkbox checked={completed} onChange={onToggleCompleted} /> | ||
| <p className="typo-headline-b-22 text-timo-black min-w-0 flex-1 wrap-break-word"> | ||
| {title} | ||
| </p> | ||
| </div> | ||
|
|
||
| <div className="flex w-full items-center justify-between"> | ||
| <div className="flex items-center gap-2"> | ||
| <div className="flex items-center gap-0.5"> | ||
| <CalendarOnIcon width={22} height={22} /> | ||
| <span className="typo-caption-r-10 text-timo-gray-900 whitespace-nowrap"> | ||
| {dateText} | ||
| </span> | ||
| </div> | ||
|
|
||
| <div className="flex items-center gap-0.5"> | ||
| <ClockOnIcon width={22} height={22} /> | ||
| <span className="typo-caption-r-10 text-timo-gray-900 w-9 text-center whitespace-nowrap"> | ||
| {durationText} | ||
| </span> | ||
| </div> | ||
| </div> | ||
|
|
||
| <PlayButton | ||
| variant={isRunning ? "stop" : "play"} | ||
| size="lg" | ||
| onClick={onTogglePlay} | ||
| > | ||
| {isRunning ? ( | ||
| <StopIcon width={24} height={24} /> | ||
| ) : ( | ||
| <PlayIcon width={24} height={24} /> | ||
| )} | ||
| </PlayButton> | ||
| </div> | ||
|
|
||
| <div className="border-timo-gray-500 flex min-h-26 w-full min-w-0 flex-col gap-3 border-t pt-3"> | ||
| {subtasks.map((subtask) => ( | ||
| <div key={subtask.subtaskId} className="flex items-center gap-2"> | ||
| <Checkbox | ||
| checked={subtask.completed} | ||
| onChange={(checked) => | ||
| onToggleSubtaskCompleted(subtask.subtaskId, checked) | ||
| } | ||
| /> | ||
| <p | ||
| className={cn( | ||
| "typo-body-r-12 min-w-0 flex-1 wrap-break-word", | ||
| subtask.completed ? "text-timo-gray-700" : "text-timo-black", | ||
| )} | ||
| > | ||
| {subtask.content} | ||
| </p> | ||
| </div> | ||
| ))} | ||
|
|
||
| {memo && ( | ||
| <p className="typo-body-r-12 text-timo-gray-800 min-w-0 wrap-break-word"> | ||
| {memo} | ||
| </p> | ||
| )} | ||
| </div> | ||
| </div> | ||
| ); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
카드 전체를 감싸는 div, semantic 태그로 바꿔볼까요?
최상위 컨테이너가 패딩/레이아웃 스타일을 가진 시각적 컨테이너라 div 자체는 허용 범위지만, 태스크 "카드"라는 의미 단위를 표현하려면 <article>이나 <section>을 쓰는 게 접근성 트리 상 더 명확해요.
As per path instructions, "div 남발 금지 → main, article, section, nav, header, footer 등 시맨틱 태그 사용".
♻️ 제안 diff
- <div className="flex w-full min-w-0 flex-col items-start gap-5 px-[34.5px]">
+ <article className="flex w-full min-w-0 flex-col items-start gap-5 px-[34.5px]">
...
- </div>
+ </article>📝 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.
| <div className="flex w-full min-w-0 flex-col items-start gap-5 px-[34.5px]"> | |
| <div className="text-timo-gray-900 flex flex-col items-start"> | |
| <p className="typo-headline-b-30">{dayNumber}</p> | |
| <p className="typo-headline-m-14">{dayOfWeek}</p> | |
| </div> | |
| <div className="flex w-full items-center gap-2.5"> | |
| <Checkbox checked={completed} onChange={onToggleCompleted} /> | |
| <p className="typo-headline-b-22 text-timo-black min-w-0 flex-1 wrap-break-word"> | |
| {title} | |
| </p> | |
| </div> | |
| <div className="flex w-full items-center justify-between"> | |
| <div className="flex items-center gap-2"> | |
| <div className="flex items-center gap-0.5"> | |
| <CalendarOnIcon width={22} height={22} /> | |
| <span className="typo-caption-r-10 text-timo-gray-900 whitespace-nowrap"> | |
| {dateText} | |
| </span> | |
| </div> | |
| <div className="flex items-center gap-0.5"> | |
| <ClockOnIcon width={22} height={22} /> | |
| <span className="typo-caption-r-10 text-timo-gray-900 w-9 text-center whitespace-nowrap"> | |
| {durationText} | |
| </span> | |
| </div> | |
| </div> | |
| <PlayButton | |
| variant={isRunning ? "stop" : "play"} | |
| size="lg" | |
| onClick={onTogglePlay} | |
| > | |
| {isRunning ? ( | |
| <StopIcon width={24} height={24} /> | |
| ) : ( | |
| <PlayIcon width={24} height={24} /> | |
| )} | |
| </PlayButton> | |
| </div> | |
| <div className="border-timo-gray-500 flex min-h-26 w-full min-w-0 flex-col gap-3 border-t pt-3"> | |
| {subtasks.map((subtask) => ( | |
| <div key={subtask.subtaskId} className="flex items-center gap-2"> | |
| <Checkbox | |
| checked={subtask.completed} | |
| onChange={(checked) => | |
| onToggleSubtaskCompleted(subtask.subtaskId, checked) | |
| } | |
| /> | |
| <p | |
| className={cn( | |
| "typo-body-r-12 min-w-0 flex-1 wrap-break-word", | |
| subtask.completed ? "text-timo-gray-700" : "text-timo-black", | |
| )} | |
| > | |
| {subtask.content} | |
| </p> | |
| </div> | |
| ))} | |
| {memo && ( | |
| <p className="typo-body-r-12 text-timo-gray-800 min-w-0 wrap-break-word"> | |
| {memo} | |
| </p> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| <article className="flex w-full min-w-0 flex-col items-start gap-5 px-[34.5px]"> | |
| <div className="text-timo-gray-900 flex flex-col items-start"> | |
| <p className="typo-headline-b-30">{dayNumber}</p> | |
| <p className="typo-headline-m-14">{dayOfWeek}</p> | |
| </div> | |
| <div className="flex w-full items-center gap-2.5"> | |
| <Checkbox checked={completed} onChange={onToggleCompleted} /> | |
| <p className="typo-headline-b-22 text-timo-black min-w-0 flex-1 wrap-break-word"> | |
| {title} | |
| </p> | |
| </div> | |
| <div className="flex w-full items-center justify-between"> | |
| <div className="flex items-center gap-2"> | |
| <div className="flex items-center gap-0.5"> | |
| <CalendarOnIcon width={22} height={22} /> | |
| <span className="typo-caption-r-10 text-timo-gray-900 whitespace-nowrap"> | |
| {dateText} | |
| </span> | |
| </div> | |
| <div className="flex items-center gap-0.5"> | |
| <ClockOnIcon width={22} height={22} /> | |
| <span className="typo-caption-r-10 text-timo-gray-900 w-9 text-center whitespace-nowrap"> | |
| {durationText} | |
| </span> | |
| </div> | |
| </div> | |
| <PlayButton | |
| variant={isRunning ? "stop" : "play"} | |
| size="lg" | |
| onClick={onTogglePlay} | |
| > | |
| {isRunning ? ( | |
| <StopIcon width={24} height={24} /> | |
| ) : ( | |
| <PlayIcon width={24} height={24} /> | |
| )} | |
| </PlayButton> | |
| </div> | |
| <div className="border-timo-gray-500 flex min-h-26 w-full min-w-0 flex-col gap-3 border-t pt-3"> | |
| {subtasks.map((subtask) => ( | |
| <div key={subtask.subtaskId} className="flex items-center gap-2"> | |
| <Checkbox | |
| checked={subtask.completed} | |
| onChange={(checked) => | |
| onToggleSubtaskCompleted(subtask.subtaskId, checked) | |
| } | |
| /> | |
| <p | |
| className={cn( | |
| "typo-body-r-12 min-w-0 flex-1 wrap-break-word", | |
| subtask.completed ? "text-timo-gray-700" : "text-timo-black", | |
| )} | |
| > | |
| {subtask.content} | |
| </p> | |
| </div> | |
| ))} | |
| {memo && ( | |
| <p className="typo-body-r-12 text-timo-gray-800 min-w-0 wrap-break-word"> | |
| {memo} | |
| </p> | |
| )} | |
| </div> | |
| </article> |
🤖 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 `@apps/timo-web/app/`[locale]/(main)/focus/_components/FocusTaskCard.tsx around
lines 42 - 112, The top-level wrapper in FocusTaskCard should use a semantic
container instead of a plain div to better represent this as a task card.
Replace the outermost container in FocusTaskCard with an appropriate semantic
tag such as article or section, keeping the existing layout and styling classes
intact. Make sure the change is applied only to the card root so the inner
structure and handlers remain unchanged.
Source: Path instructions
| })); | ||
| }; | ||
|
|
||
| const handleAddTime = () => {}; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
handleAddTime, 조용히 비어있네요 (TODO 주석마저 없이).
다른 핸들러들은 // TODO: API 주석으로 나중에 작업임을 표시해뒀는데, handleAddTime만 빈 화살괄호로 덩그러니 있어요. 리뷰어가 "이거 의도된 거 맞나?" 갸우뚱하게 만드는 포인트입니다.
- const handleAddTime = () => {};
+ const handleAddTime = () => {
+ // TODO: 시간 추가 로직 (API 연동 및 타이머 엔진 연결 후속 작업)
+ };원하시면 이 부분 이슈로 등록해드릴까요?
📝 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.
| const handleAddTime = () => {}; | |
| const handleAddTime = () => { | |
| // TODO: 시간 추가 로직 (API 연동 및 타이머 엔진 연결 후속 작업) | |
| }; |
🤖 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
`@apps/timo-web/app/`[locale]/(main)/focus/_containers/FocusSessionContainer.tsx
at line 42, The handleAddTime placeholder in FocusSessionContainer is left as an
empty function, unlike the other handlers that clearly mark pending work with a
TODO comment. Update handleAddTime to match the existing handler pattern in
FocusSessionContainer by adding an explicit TODO marker (or otherwise making the
intentional stub obvious) so it is clear this is unfinished work rather than an
accidental no-op.
| export interface FocusTaskSubtask { | ||
| subtaskId: number; | ||
| content: string; | ||
| completed: boolean; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Boolean 네이밍 컨벤션 살짝 아쉬워요 😅
completed는 is/has/should/can 접두사가 없어서 컨벤션 위반이에요. isCompleted로 바꾸면 isRunning과도 짝이 맞아서 훨씬 자연스러워집니다. 이 타입을 task-mock.ts와 FocusTaskCard.tsx에서도 그대로 쓰고 있어서, 한 번에 고치면 전체가 정리돼요.
As per path instructions, "Boolean 변수명: is, has, should, can 접두사".
♻️ 제안 diff
export interface FocusTaskSubtask {
subtaskId: number;
content: string;
- completed: boolean;
+ isCompleted: boolean;
}
export interface FocusTask {
taskId: number;
title: string;
- completed: boolean;
+ isCompleted: boolean;
scheduledDate: Date;
durationSeconds: number;
isRunning: boolean;
subtasks: FocusTaskSubtask[];
memo?: string;
}Also applies to: 10-10
🤖 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 `@apps/timo-web/app/`[locale]/(main)/focus/_types/task-type.ts at line 4, The
boolean field name in the Task type does not follow the is/has/should/can naming
convention. Rename the `completed` property in `Task` to `isCompleted`, then
update every usage that reads or writes this field in `task-mock.ts` and
`FocusTaskCard.tsx` so the type and consumers stay aligned with the new name.
Source: Path instructions
- 태스크 카드가 무한정 찌그러지지 않도록 최소 너비를 지정했습니다 - 화면이 좁아지면 축소 대신 페이지에 가로 스크롤이 생기도록 했습니다
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
apps/timo-web/app/[locale]/(main)/focus/_containers/FocusSessionContainer.tsx (2)
89-95: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueTimer props가 하드코딩되어 있습니다 — 의도적인지 확인 부탁드립니다.
time="10:30",plannedLabel="12M",progress={70}이 정적으로 하드코딩되어 있습니다. PR objectives에 "mock data for now"라고 명시되어 있어 의도적일 수 있으나,task.durationSeconds에서 파생된 값이나 별도 목데이터 상수로 관리하면 후속 API 연동 시 교체 포인트가 명확해집니다.♻️ 제안: 목데이터 상수로 분리
+ // _mocks/timer-mock.ts 또는 상단에 정의 + const timerMock = { + time: "10:30", + plannedLabel: "12M", + progress: 70, + } as const; <Timer - icon={<TimerOnIcon />} - time="10:30" - plannedLabel="12M" - progress={70} + icon={<TimerOnIcon />} + time={timerMock.time} + plannedLabel={timerMock.plannedLabel} + progress={timerMock.progress} size="lg" />🤖 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 `@apps/timo-web/app/`[locale]/(main)/focus/_containers/FocusSessionContainer.tsx around lines 89 - 95, The Timer props in FocusSessionContainer are currently hardcoded, so either confirm this is intentional mock data or move the values into a dedicated mock-data constant or derive them from task.durationSeconds. Update the Timer usage in FocusSessionContainer and any related helper logic so time, plannedLabel, and progress come from a single replaceable source rather than inline literals.
22-22: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value
new Date()가 매 렌더마다 새로 생성됩니다.
const today = new Date()는 컴포넌트가 리렌더링될 때마다 새 Date 객체를 생성합니다. 타이머 토글 등 상태 변경 시 불필요한 재할당이 발생합니다.useMemo로 감싸면 날짜 값이 동일한 렌더 사이클에서 안정적으로 유지됩니다.♻️ 제안 diff
+import { useMemo, useState } from "react"; export const FocusSessionContainer = () => { const [task, setTask] = useState<FocusTask>(focusTaskMock); - const today = new Date(); + const today = useMemo(() => new Date(), []);🤖 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 `@apps/timo-web/app/`[locale]/(main)/focus/_containers/FocusSessionContainer.tsx at line 22, `FocusSessionContainer`에서 생성되는 `today` 값이 매 렌더마다 새 Date 객체로 할당되고 있습니다. 이 값을 컴포넌트 렌더 사이클 동안 안정적으로 유지하도록 `FocusSessionContainer` 내부의 `today` 계산을 `useMemo`로 감싸고, 의존성이 없으면 한 번만 생성되도록 조정하세요. `today`를 사용하는 로직 전체가 동일한 memoized 값을 참조하도록 맞춰 주세요.apps/timo-web/app/[locale]/(main)/focus/_components/FocusTaskCard.tsx (1)
85-103: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value서브태스크 영역도 시맨틱 태그를 고려해보세요.
서브태스크 목록을 감싸는
<div>(line 85)와 각 서브태스크 아이템의<div>(line 87)가 의미론적으로는 리스트 구조입니다.<ul>/<li>를 사용하면 스크린 리더에서 "목록, N개 항목"으로 읽어주어 접근성이 개선됩니다.♻️ 제안 diff
- <div className="border-timo-gray-500 flex min-h-26 w-full min-w-0 flex-col gap-3 border-t pt-3"> + <ul className="border-timo-gray-500 flex min-h-26 w-full min-w-0 flex-col gap-3 border-t pt-3"> {subtasks.map((subtask) => ( - <div key={subtask.subtaskId} className="flex items-center gap-2"> + <li key={subtask.subtaskId} className="flex items-center gap-2"> ... - </div> + </li> ))} ... - </div> + </ul>🤖 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 `@apps/timo-web/app/`[locale]/(main)/focus/_components/FocusTaskCard.tsx around lines 85 - 103, The subtask section in FocusTaskCard is rendered with generic containers even though it is a list structure. Update the wrapper and item markup in FocusTaskCard to use semantic list elements like a list container and list items around the subtasks map, while keeping the existing Checkbox, subtaskId keying, and onToggleSubtaskCompleted behavior unchanged.Source: Path instructions
♻️ Duplicate comments (1)
apps/timo-web/app/[locale]/(main)/focus/_components/FocusTaskCard.tsx (1)
42-42: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value과거 리뷰에서 제안된
<article>시맨틱 태그 전환이 아직 반영되지 않았습니다.이전 리뷰에서 최상위
<div>를<article>로 변경하라는 제안이 있었으나, 현재 코드는 여전히<div>를 사용하고 있습니다. 다만 path instructions에 따르면 "컴포넌트 자체가 시각적 컨테이너(배경색·크기·border-radius 등 스타일을 정의)인 경우는 div 허용"이므로, 현재px-[34.5px] py-8패딩만 있고 배경색·border-radius 등은 없어 시각적 컨테이너라기보단 그룹핑 목적에 가깝습니다.<article>이 접근성 트리에서 태스크 카드라는 의미 단위를 더 명확히 전달합니다.As per path instructions, "div 남발 금지 → main, article, section, nav, header, footer 등 시맨틱 태그 사용".
♻️ 제안 diff
- <div className="flex w-full min-w-80 flex-col items-start gap-5 px-[34.5px] py-8"> + <article className="flex w-full min-w-80 flex-col items-start gap-5 px-[34.5px] py-8"> ... - </div> + </article>🤖 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 `@apps/timo-web/app/`[locale]/(main)/focus/_components/FocusTaskCard.tsx at line 42, Update FocusTaskCard so the top-level wrapper is an <article> instead of the current <div>, since this component represents a meaningful task card rather than a purely visual container. Keep the existing styling and structure intact, but change the outer element in FocusTaskCard to the semantic tag that better identifies the card as a self-contained content unit.Source: Path instructions
🤖 Prompt for all review comments with 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.
Outside diff comments:
In `@apps/timo-web/app/`[locale]/(main)/focus/_components/FocusTaskCard.tsx:
- Around line 85-103: The subtask section in FocusTaskCard is rendered with
generic containers even though it is a list structure. Update the wrapper and
item markup in FocusTaskCard to use semantic list elements like a list container
and list items around the subtasks map, while keeping the existing Checkbox,
subtaskId keying, and onToggleSubtaskCompleted behavior unchanged.
In
`@apps/timo-web/app/`[locale]/(main)/focus/_containers/FocusSessionContainer.tsx:
- Around line 89-95: The Timer props in FocusSessionContainer are currently
hardcoded, so either confirm this is intentional mock data or move the values
into a dedicated mock-data constant or derive them from task.durationSeconds.
Update the Timer usage in FocusSessionContainer and any related helper logic so
time, plannedLabel, and progress come from a single replaceable source rather
than inline literals.
- Line 22: `FocusSessionContainer`에서 생성되는 `today` 값이 매 렌더마다 새 Date 객체로 할당되고
있습니다. 이 값을 컴포넌트 렌더 사이클 동안 안정적으로 유지하도록 `FocusSessionContainer` 내부의 `today` 계산을
`useMemo`로 감싸고, 의존성이 없으면 한 번만 생성되도록 조정하세요. `today`를 사용하는 로직 전체가 동일한 memoized 값을
참조하도록 맞춰 주세요.
---
Duplicate comments:
In `@apps/timo-web/app/`[locale]/(main)/focus/_components/FocusTaskCard.tsx:
- Line 42: Update FocusTaskCard so the top-level wrapper is an <article> instead
of the current <div>, since this component represents a meaningful task card
rather than a purely visual container. Keep the existing styling and structure
intact, but change the outer element in FocusTaskCard to the semantic tag that
better identifies the card as a self-contained content unit.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 391371a9-c818-4aa0-b2c7-c5a361038e42
📒 Files selected for processing (2)
apps/timo-web/app/[locale]/(main)/focus/_components/FocusTaskCard.tsxapps/timo-web/app/[locale]/(main)/focus/_containers/FocusSessionContainer.tsx
- py-8(상하 패딩)을 pb-8(하단 패딩만)로 변경했습니다
- 카드보다 아이템에 가까운 컴포넌트라 용도에 맞게 리네임했습니다 - 루트 태그를 div에서 section으로 변경했습니다

ISSUE 🔗
close #109
What is this PR? 🔍
Figma 집중 모드(Focus) 디자인을 기반으로 타이머 영역과 태스크 상세 영역을 구현하고, 두 영역의 재생/완료 상태를 하나의 컨테이너에서 공유하도록 만들었습니다.
배경
focus/page.tsx는Timer/TimerControls에 빈 값(icon={undefined},time="",size="sm")만 전달하는 플레이스홀더 상태였습니다.FocusSessionContainer하나로 통합해 단일 상태(task)를 공유하고, 표현 컴포넌트(FocusTaskCard,Timer,TimerControls)는 순수 프레젠테이션으로 유지했습니다.타이머 영역
Timer,size="lg")와 재생/종료/시간추가 컨트롤(TimerControls)을 실제로 연결했습니다.page.tsx는 Server Component라 인터랙티브한 재생 상태를 가질 수 없어 별도 client 컨테이너가 필요했습니다.TimerPanel.tsx([FEAT] 타임박스 타이머 우측 사이드바 구현 #90)에서 확립된 "재생 버튼만 강조" 패턴과 어긋나 다시 되돌렸습니다. 여러 화면에서 재사용되는 공용 컴포넌트라 기존 합의를 따르는 쪽을 선택했습니다.태스크 상세 영역
FocusTaskCard를 추가했습니다.1465-29545)과 대응되는 화면이 없었습니다.FocusTaskCard는 props만 받는 순수 UI(_components)이고,FocusSessionContainer가 mock 데이터(focusTaskMock)와 상태 변경 로직을 소유합니다. 상위 체크박스를 체크하면 하위 테스크 전체가 함께 체크되지만, 하위 테스크를 체크한다고 상위 상태가 바뀌지는 않는 단방향 cascade로 구현했습니다. 날짜/시간 포맷 로직은 home 도메인의 유사 유틸과 겹치지만, 도메인 간 직접 import를 금지하는 컨벤션(docs/architecture/structure.md) 때문에focus/_utils에 별도로 작성했습니다.focusTaskMock(단일 mock task) 기준입니다. "오늘 투두 전체 완료" / "수행할 투두 없음" empty state(Figma node1762-43383)는 프로토타입까지 구현했으나 백엔드 API가 준비되지 않아 이번 PR에서는 롤백하고 후속 작업으로 남겼습니다.재생 상태 동기화
task.isRunning)를 공유하고, 타이머의 종료 버튼을 누르면 태스크와 하위 테스크가 모두 완료 처리되도록 연결했습니다.FocusSessionContainer의handleTogglePlay/handleEnd가task상태를 갱신하고, 이 값을FocusTaskCard.isRunning과TimerControls.isRunning에 동일하게 전달합니다.To Reviewers
타이머/태스크 두 영역을
FocusSessionContainer하나로 합친 구조가 적절한지 봐주세요. 실제 투두 리스트 API가 붙으면 이 컨테이너가 데이터 페칭(useQuery)까지 맡게 될 예정입니다.FocusTaskCard의 날짜/시간 포맷 유틸을 home 도메인과 별도로 만들었는데(도메인 경계 준수 목적), 중복이 계속 늘어나면 공유lib로 추출이 필요할 수 있습니다."오늘 투두 전체 완료" / "수행할 투두 없음" empty state는 API 연동 전까지 보류했으며 후속 PR로 진행할 예정입니다.
Screenshot 📷
로컬 dev 서버(
/ko/focus)에서 Playwright로 직접 캡처해 정지/재생 상태, 체크박스 cascade, 긴 메모 줄바꿈 동작을 확인했습니다. 이미지 파일은 로컬 스크린샷이라 이 PR 본문에는 첨부하지 않았습니다.Test Checklist ✔
pnpm check-types통과pnpm lint통과/ko/focus로컬 dev 서버 렌더링 확인 (Playwright)pnpm build— 미실행: CI에서 확인 예정