Create Week09 Mission 1,2,3#78
Conversation
📝 WalkthroughWalkthrough네 개의 주요 React 학습 프로젝트가 추가됨: Redux 기반 단순 장바구니(Mission01), Redux + Modal 장바구니(Mission02), Zustand 기반 장바구니(Mission03), useReducer 연습(Practice01). 각 프로젝트는 TypeScript, Vite, Tailwind CSS로 구성된 완전한 SPA 애플리케이션. ChangesRedux Cart - Simple (Mission01)
Redux Cart with Modal (Mission02)
Zustand Cart Implementation (Mission03)
useReducer Learning (Practice01)
Estimated code review effort🎯 2 (Simple) | ⏱️ ~15 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 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 |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (5)
Week09/Mission03/src/App.tsx (1)
6-6: ⚡ Quick winZustand 전체 스토어 구독 대신 selector 구독으로 바꿔주세요.
Line 6은 스토어 전체를 구독해서,
amount/isOpen과 무관한 변경에도App이 다시 렌더링됩니다. selector 분리 구독이 더 안정적입니다.제안 diff
export default function App() { - const { amount, isOpen } = usePlaylistStore(); + const amount = usePlaylistStore((state) => state.amount); + const isOpen = usePlaylistStore((state) => state.isOpen);🤖 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 `@Week09/Mission03/src/App.tsx` at line 6, The App component currently subscribes to the entire Zustand store via usePlaylistStore(), causing unrelated store changes to rerender App; instead subscribe to only the needed slices by replacing the whole-store call with two selector subscriptions such as using usePlaylistStore(state => state.amount) for amount and usePlaylistStore(state => state.isOpen) for isOpen (or a single selector that returns an object and shallow compares if you prefer), updating references in App to those selectors to avoid full-store re-renders.Week09/Mission03/src/components/CartContainer.tsx (1)
7-7: ⚡ Quick win
CartContainer도 selector 기반으로 구독 범위를 줄여주세요.Line 7에서 전체 스토어를 구독하고 있어 불필요한 리스트 재렌더링이 발생할 수 있습니다.
제안 diff
export default function CartContainer() { - const { amount, cartItems, openModal, total } = usePlaylistStore(); + const amount = usePlaylistStore((state) => state.amount); + const cartItems = usePlaylistStore((state) => state.cartItems); + const total = usePlaylistStore((state) => state.total); + const openModal = usePlaylistStore((state) => state.openModal);🤖 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 `@Week09/Mission03/src/components/CartContainer.tsx` at line 7, CartContainer is subscribing to the whole store by destructuring usePlaylistStore() which causes unnecessary re-renders; change to selector-based subscriptions e.g. replace const { amount, cartItems, openModal, total } = usePlaylistStore(); with selector calls that pick only needed slices (either a single object selector: usePlaylistStore(s => ({ amount: s.amount, cartItems: s.cartItems, openModal: s.openModal, total: s.total }), or separate calls like usePlaylistStore(s => s.cartItems) and usePlaylistStore(s => s.amount) etc.), and if needed pass a shallow/equality function to avoid re-rendering when values are referentially equal — refer to CartContainer and usePlaylistStore for the exact symbols to change.Week09/Mission01/src/constants/cartItems.ts (1)
1-7: ⚡ Quick win
price를string대신number로 관리하는 쪽이 안전합니다.금액을 숫자 타입으로 정의하면 계산 시 런타임 파싱을 제거할 수 있고, NaN 리스크를 줄일 수 있습니다.
리팩터 예시
export interface CartItem { id: string; title: string; singer: string; - price: string; + price: number; img: string; amount: number; } const cartItems: CartItem[] = [ { @@ - price: "25000", + price: 25000, @@ - price: "18000", + price: 18000, @@ - price: "28000", + price: 28000, @@ - price: "20000", + price: 20000, @@ - price: "30000", + price: 30000, @@ - price: "12000", + price: 12000, @@ - price: "32000", + price: 32000, @@ - price: "22000", + price: 22000, @@ - price: "20000", + price: 20000, @@ - price: "25000", + price: 25000, @@ - price: "23000", + price: 23000, @@ - price: "21000", + price: 21000,Also applies to: 15-106
🤖 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 `@Week09/Mission01/src/constants/cartItems.ts` around lines 1 - 7, Change the CartItem type so its price field is a number (update interface CartItem in constants/cartItems.ts from price: string to price: number) and then update all sites that construct or read CartItem.price (parsers, initial state, action payloads, components, and any calculation logic) to use numeric values (parse incoming strings to Number when creating CartItem objects and remove string-to-number conversions where totals are computed). Ensure tests/usage that assumed string prices are updated accordingly to use numbers and handle NaN validation at creation time.Week09/Mission02/src/components/Modal.tsx (1)
14-42: ⚡ Quick win모달 키보드 접근성(초기 포커스/ESC 닫기)을 보강하는 것이 좋습니다.
현재도 동작은 하지만, 모달 오픈 시 포커스 이동과
Escape닫기를 추가하면 접근성이 더 안정적입니다.♿ 제안 수정안
+import { useEffect, useRef, type KeyboardEvent } from "react"; import { clearCart } from "../features/cart/cartSlice"; import { closeModal } from "../features/modal/modalSlice"; import { useAppDispatch } from "../hooks/redux"; @@ export default function Modal() { const dispatch = useAppDispatch(); + const cancelButtonRef = useRef<HTMLButtonElement>(null); + + useEffect(() => { + cancelButtonRef.current?.focus(); + }, []); const handleConfirm = () => { dispatch(clearCart()); dispatch(closeModal()); }; + + const handleKeyDown = (event: KeyboardEvent<HTMLElement>) => { + if (event.key === "Escape") { + dispatch(closeModal()); + } + }; @@ <section role="dialog" aria-modal="true" aria-labelledby="delete-cart-title" + tabIndex={-1} + onKeyDown={handleKeyDown} className="w-full max-w-[294px] rounded-lg bg-white px-8 py-9 text-center shadow-xl" > @@ <button + ref={cancelButtonRef} type="button" onClick={() => dispatch(closeModal())} className="rounded-md bg-slate-200 px-6 py-3 text-lg font-bold text-slate-700 transition hover:bg-slate-300" >🤖 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 `@Week09/Mission02/src/components/Modal.tsx` around lines 14 - 42, The modal needs keyboard accessibility: add an initial focus and Escape-to-close behavior. In the Modal component, create a ref for the primary interactive element (e.g., noButtonRef for the "아니요" button) and in a useEffect set focus(noButtonRef.current) when the modal mounts and return a cleanup to restore focus if desired; also register a keydown listener in that same useEffect that listens for "Escape" and calls dispatch(closeModal()), and remove the listener on unmount. Keep existing handlers (handleConfirm, dispatch(closeModal())) but ensure the role="dialog" container has aria-modal="true" (already present) and that the focused element is inside the dialog so keyboard users can operate it immediately.Week09/Mission02/src/constants/cartItems.ts (1)
5-5: ⚡ Quick win
price는 문자열 대신 숫자 타입으로 고정하는 게 안전합니다.Line 5처럼
price: string으로 두면 합계 계산 시 매번 파싱이 필요하고, 포맷이 섞이는 순간NaN리스크가 생깁니다. 장바구니 도메인에서는 숫자 모델이 더 안정적입니다.🔧 제안 수정안
export interface CartItem { id: string; title: string; singer: string; - price: string; + price: number; img: string; amount: number; } @@ - price: "25000", + price: 25000, @@ - price: "18000", + price: 18000, @@ - price: "28000", + price: 28000, @@ - price: "20000", + price: 20000, @@ - price: "30000", + price: 30000, @@ - price: "12000", + price: 12000, @@ - price: "32000", + price: 32000, @@ - price: "22000", + price: 22000, @@ - price: "20000", + price: 20000, @@ - price: "25000", + price: 25000, @@ - price: "23000", + price: 23000, @@ - price: "21000", + price: 21000,Also applies to: 15-15, 23-23, 31-31, 39-39, 47-47, 55-55, 63-63, 71-71, 79-79, 87-87, 95-95, 103-103
🤖 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 `@Week09/Mission02/src/constants/cartItems.ts` at line 5, The cart item type currently declares price as a string which risks NaN and parsing overhead; change the price property in the cart item type/constant definitions from string to number (i.e., replace all occurrences of "price: string" with "price: number") and ensure any literal values in the corresponding cart item objects are numeric (no quotes) so totals and arithmetic (e.g., sum/total calculations) operate on numbers without parsing; update any related initialization or tests that supply string prices to use numeric literals.
🤖 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 `@Week09/Mission01/src/components/CartItem.tsx`:
- Around line 27-29: The price is displayed with a hardcoded "$" in the CartItem
component; replace this with a Korean won formatted string (use
Intl.NumberFormat with 'ko-KR' and currency 'KRW' or a shared formatPrice
utility) and render the formatted value instead of `${item.price}`; update the
JSX in the CartItem render where item.price is used so all prices match the 원
currency format.
In `@Week09/Mission01/src/store/cartSlice.ts`:
- Around line 36-61: The increase, decrease and removeItem reducers update
state.cartItems but do not update aggregated fields (amount/total), causing
inconsistent state; fix by invoking the slice's existing calculateTotals logic
after each mutation (or recomputing totals inline) so totals stay in sync—e.g.,
after modifying state.cartItems in increase, decrease, and removeItem call
calculateTotals(state) (or update state.total/state.amount using the same
reduction logic) to keep the invariants.
In `@Week09/Mission02/index.html`:
- Line 6: 제목이 잘못되어 있습니다: index.html의 <title> 태그 값이 "Week09 Mission1"으로 되어 있으니 이를
현재 프로젝트명에 맞게 "Week09 Mission02" 또는 단순히 "Mission02"로 변경하세요; 대상은 index.html의
<title> 요소이며 파일 내 <title> 태그만 수정하면 됩니다.
In `@Week09/Mission02/src/components/CartItem.tsx`:
- Around line 27-29: The CartItem component is rendering item.price with a
leading "$" which conflicts with the total's Korean won formatting; update the
CartItem.tsx rendering in the CartItem component to format item.price using the
same Korean won formatter used for the total (e.g., Intl.NumberFormat('ko-KR', {
style: 'currency', currency: 'KRW' }) or the project's currency helper) so the
list rows display prices in 원 and match the total; locate the JSX that currently
shows "${item.price}" and replace it with the shared KRW formatting call for
item.price.
In `@Week09/Mission02/src/features/cart/cartSlice.ts`:
- Around line 36-61: The reducers increase, decrease, and removeItem update
cartItems but do not update derived totals (amount and total), causing a
temporary state mismatch; fix by ensuring each of these reducers also updates
state.amount and state.total immediately after modifying cartItems (either by
invoking the existing calculateTotals reducer logic directly like
calculateTotals(state) or by recomputing amount/total inline) so the store
remains consistent; update the implementations of increase, decrease, and
removeItem to recalc totals right after changing cartItems.
In `@Week09/Mission03/index.html`:
- Line 6: HTML 문서의 <title> 요소 값이 "Week09 Mission1"으로 잘못되어 현재 프로젝트와 불일치합니다;
index.html의 <title> 내용을 현재 미션명(예: "Week09 Mission03" 또는 프로젝트명)으로 변경하여 브라우저 탭과
북마크에 맞게 정확한 미션명을 반영하세요.
In `@Week09/Mission03/src/components/CartItem.tsx`:
- Line 27: The currency symbol in the CartItem component is inconsistent:
replace the hardcoded "$" at the interpolation of item.price in CartItem.tsx
with the same currency/format used elsewhere (Korean won) or use a centralized
formatter; update the interpolation that currently reads `${item.price}` to
render with "원" (e.g., `${item.price}원`) or call the shared formatter function
(e.g., formatCurrency(item.price)) so the displayed unit matches the cart total
and app-wide currency.
In `@Week09/Practice01/src/components/UseReducerCompany.tsx`:
- Around line 59-70: Add accessible labeling and a live error region: give the
text input (currently bound to the department state via value={department} and
onChange={handleChange}) a visible label element tied with htmlFor/id, mark the
input with aria-invalid={Boolean(state.error)} and add aria-describedby that
points to the error message id; convert the error <p> (which currently renders
{state.error}) into a role="alert" or aria-live="assertive" region with a stable
id so screen readers are notified when state.error changes. Ensure identifiers
referenced (department input, handleChange, state.error, and the error <p>) are
updated consistently.
---
Nitpick comments:
In `@Week09/Mission01/src/constants/cartItems.ts`:
- Around line 1-7: Change the CartItem type so its price field is a number
(update interface CartItem in constants/cartItems.ts from price: string to
price: number) and then update all sites that construct or read CartItem.price
(parsers, initial state, action payloads, components, and any calculation logic)
to use numeric values (parse incoming strings to Number when creating CartItem
objects and remove string-to-number conversions where totals are computed).
Ensure tests/usage that assumed string prices are updated accordingly to use
numbers and handle NaN validation at creation time.
In `@Week09/Mission02/src/components/Modal.tsx`:
- Around line 14-42: The modal needs keyboard accessibility: add an initial
focus and Escape-to-close behavior. In the Modal component, create a ref for the
primary interactive element (e.g., noButtonRef for the "아니요" button) and in a
useEffect set focus(noButtonRef.current) when the modal mounts and return a
cleanup to restore focus if desired; also register a keydown listener in that
same useEffect that listens for "Escape" and calls dispatch(closeModal()), and
remove the listener on unmount. Keep existing handlers (handleConfirm,
dispatch(closeModal())) but ensure the role="dialog" container has
aria-modal="true" (already present) and that the focused element is inside the
dialog so keyboard users can operate it immediately.
In `@Week09/Mission02/src/constants/cartItems.ts`:
- Line 5: The cart item type currently declares price as a string which risks
NaN and parsing overhead; change the price property in the cart item
type/constant definitions from string to number (i.e., replace all occurrences
of "price: string" with "price: number") and ensure any literal values in the
corresponding cart item objects are numeric (no quotes) so totals and arithmetic
(e.g., sum/total calculations) operate on numbers without parsing; update any
related initialization or tests that supply string prices to use numeric
literals.
In `@Week09/Mission03/src/App.tsx`:
- Line 6: The App component currently subscribes to the entire Zustand store via
usePlaylistStore(), causing unrelated store changes to rerender App; instead
subscribe to only the needed slices by replacing the whole-store call with two
selector subscriptions such as using usePlaylistStore(state => state.amount) for
amount and usePlaylistStore(state => state.isOpen) for isOpen (or a single
selector that returns an object and shallow compares if you prefer), updating
references in App to those selectors to avoid full-store re-renders.
In `@Week09/Mission03/src/components/CartContainer.tsx`:
- Line 7: CartContainer is subscribing to the whole store by destructuring
usePlaylistStore() which causes unnecessary re-renders; change to selector-based
subscriptions e.g. replace const { amount, cartItems, openModal, total } =
usePlaylistStore(); with selector calls that pick only needed slices (either a
single object selector: usePlaylistStore(s => ({ amount: s.amount, cartItems:
s.cartItems, openModal: s.openModal, total: s.total }), or separate calls like
usePlaylistStore(s => s.cartItems) and usePlaylistStore(s => s.amount) etc.),
and if needed pass a shallow/equality function to avoid re-rendering when values
are referentially equal — refer to CartContainer and usePlaylistStore for the
exact symbols to change.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: d29e2ef1-b712-49e8-bdf5-583d02ef0e25
⛔ Files ignored due to path filters (4)
Week09/Mission01/package-lock.jsonis excluded by!**/package-lock.jsonWeek09/Mission02/package-lock.jsonis excluded by!**/package-lock.jsonWeek09/Mission03/package-lock.jsonis excluded by!**/package-lock.jsonWeek09/Practice01/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (61)
Week09/Mission01/.gitignoreWeek09/Mission01/index.htmlWeek09/Mission01/package.jsonWeek09/Mission01/src/App.tsxWeek09/Mission01/src/components/CartContainer.tsxWeek09/Mission01/src/components/CartItem.tsxWeek09/Mission01/src/constants/cartItems.tsWeek09/Mission01/src/hooks/redux.tsWeek09/Mission01/src/index.cssWeek09/Mission01/src/main.tsxWeek09/Mission01/src/store/cartSlice.tsWeek09/Mission01/src/store/store.tsWeek09/Mission01/tsconfig.app.jsonWeek09/Mission01/tsconfig.jsonWeek09/Mission01/tsconfig.node.jsonWeek09/Mission01/vite.config.tsWeek09/Mission02/.gitignoreWeek09/Mission02/index.htmlWeek09/Mission02/package.jsonWeek09/Mission02/src/App.tsxWeek09/Mission02/src/components/CartContainer.tsxWeek09/Mission02/src/components/CartItem.tsxWeek09/Mission02/src/components/Modal.tsxWeek09/Mission02/src/constants/cartItems.tsWeek09/Mission02/src/features/cart/cartSlice.tsWeek09/Mission02/src/features/modal/modalSlice.tsWeek09/Mission02/src/hooks/redux.tsWeek09/Mission02/src/index.cssWeek09/Mission02/src/main.tsxWeek09/Mission02/src/store/store.tsWeek09/Mission02/tsconfig.app.jsonWeek09/Mission02/tsconfig.jsonWeek09/Mission02/tsconfig.node.jsonWeek09/Mission02/vite.config.tsWeek09/Mission03/.gitignoreWeek09/Mission03/index.htmlWeek09/Mission03/package.jsonWeek09/Mission03/src/App.tsxWeek09/Mission03/src/components/CartContainer.tsxWeek09/Mission03/src/components/CartItem.tsxWeek09/Mission03/src/components/Modal.tsxWeek09/Mission03/src/constants/cartItems.tsWeek09/Mission03/src/index.cssWeek09/Mission03/src/main.tsxWeek09/Mission03/src/store/usePlaylistStore.tsWeek09/Mission03/tsconfig.app.jsonWeek09/Mission03/tsconfig.jsonWeek09/Mission03/tsconfig.node.jsonWeek09/Mission03/vite.config.tsWeek09/Practice01/.gitignoreWeek09/Practice01/index.htmlWeek09/Practice01/package.jsonWeek09/Practice01/src/App.tsxWeek09/Practice01/src/components/UseReducerCompany.tsxWeek09/Practice01/src/components/UseReducerPage.tsxWeek09/Practice01/src/main.tsxWeek09/Practice01/src/style.cssWeek09/Practice01/tsconfig.app.jsonWeek09/Practice01/tsconfig.jsonWeek09/Practice01/tsconfig.node.jsonWeek09/Practice01/vite.config.ts
| <p className="mt-1 text-[20px] font-extrabold leading-tight text-slate-800"> | ||
| ${item.price} | ||
| </p> |
There was a problem hiding this comment.
가격 표기를 원화 포맷으로 통일해 주세요.
아이템 가격이 $로 표시되어 총액(원)과 통화 표기가 다릅니다. 동일한 원화 포맷으로 맞추는 것이 좋습니다.
수정 예시
- <p className="mt-1 text-[20px] font-extrabold leading-tight text-slate-800">
- ${item.price}
- </p>
+ <p className="mt-1 text-[20px] font-extrabold leading-tight text-slate-800">
+ {Number(item.price).toLocaleString("ko-KR")}원
+ </p>📝 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.
| <p className="mt-1 text-[20px] font-extrabold leading-tight text-slate-800"> | |
| ${item.price} | |
| </p> | |
| <p className="mt-1 text-[20px] font-extrabold leading-tight text-slate-800"> | |
| {Number(item.price).toLocaleString("ko-KR")}원 | |
| </p> |
🤖 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 `@Week09/Mission01/src/components/CartItem.tsx` around lines 27 - 29, The price
is displayed with a hardcoded "$" in the CartItem component; replace this with a
Korean won formatted string (use Intl.NumberFormat with 'ko-KR' and currency
'KRW' or a shared formatPrice utility) and render the formatted value instead of
`${item.price}`; update the JSX in the CartItem render where item.price is used
so all prices match the 원 currency format.
| increase: (state, action: PayloadAction<string>) => { | ||
| const item = state.cartItems.find((cartItem) => cartItem.id === action.payload); | ||
|
|
||
| if (item) { | ||
| item.amount += 1; | ||
| } | ||
| }, | ||
| decrease: (state, action: PayloadAction<string>) => { | ||
| const item = state.cartItems.find((cartItem) => cartItem.id === action.payload); | ||
|
|
||
| if (!item) return; | ||
|
|
||
| if (item.amount === 1) { | ||
| state.cartItems = state.cartItems.filter( | ||
| (cartItem) => cartItem.id !== action.payload, | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| item.amount -= 1; | ||
| }, | ||
| removeItem: (state, action: PayloadAction<string>) => { | ||
| state.cartItems = state.cartItems.filter( | ||
| (cartItem) => cartItem.id !== action.payload, | ||
| ); | ||
| }, |
There was a problem hiding this comment.
장바구니 변경 액션이 amount/total을 즉시 동기화하지 않습니다.
현재는 increase/decrease/removeItem 이후 집계값이 자동으로 맞춰지지 않아, calculateTotals를 따로 호출하지 않으면 상태가 불일치합니다. 슬라이스 내부에서 불변식을 유지하도록 각 변경 액션에서 totals를 함께 갱신하는 게 안전합니다.
수정 예시
increase: (state, action: PayloadAction<string>) => {
const item = state.cartItems.find((cartItem) => cartItem.id === action.payload);
if (item) {
item.amount += 1;
+ const totals = getTotals(state.cartItems);
+ state.amount = totals.amount;
+ state.total = totals.total;
}
},
decrease: (state, action: PayloadAction<string>) => {
const item = state.cartItems.find((cartItem) => cartItem.id === action.payload);
if (!item) return;
if (item.amount === 1) {
state.cartItems = state.cartItems.filter(
(cartItem) => cartItem.id !== action.payload,
);
- return;
+ } else {
+ item.amount -= 1;
}
-
- item.amount -= 1;
+ const totals = getTotals(state.cartItems);
+ state.amount = totals.amount;
+ state.total = totals.total;
},
removeItem: (state, action: PayloadAction<string>) => {
state.cartItems = state.cartItems.filter(
(cartItem) => cartItem.id !== action.payload,
);
+ const totals = getTotals(state.cartItems);
+ state.amount = totals.amount;
+ state.total = totals.total;
},🤖 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 `@Week09/Mission01/src/store/cartSlice.ts` around lines 36 - 61, The increase,
decrease and removeItem reducers update state.cartItems but do not update
aggregated fields (amount/total), causing inconsistent state; fix by invoking
the slice's existing calculateTotals logic after each mutation (or recomputing
totals inline) so totals stay in sync—e.g., after modifying state.cartItems in
increase, decrease, and removeItem call calculateTotals(state) (or update
state.total/state.amount using the same reduction logic) to keep the invariants.
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>Week09 Mission1</title> |
There was a problem hiding this comment.
브라우저 탭 제목을 Mission02로 맞춰주세요.
Line 6의 <title>이 "Week09 Mission1"로 되어 있어 현재 프로젝트(Mission02)와 불일치합니다.
수정 예시
- <title>Week09 Mission1</title>
+ <title>Week09 Mission2</title>📝 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.
| <title>Week09 Mission1</title> | |
| <title>Week09 Mission2</title> |
🤖 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 `@Week09/Mission02/index.html` at line 6, 제목이 잘못되어 있습니다: index.html의 <title> 태그
값이 "Week09 Mission1"으로 되어 있으니 이를 현재 프로젝트명에 맞게 "Week09 Mission02" 또는 단순히
"Mission02"로 변경하세요; 대상은 index.html의 <title> 요소이며 파일 내 <title> 태그만 수정하면 됩니다.
| <p className="mt-1 text-[20px] font-extrabold leading-tight text-slate-800"> | ||
| ${item.price} | ||
| </p> |
There was a problem hiding this comment.
아이템 가격 통화 표기가 총액 표기와 불일치합니다.
리스트 행은 $를 쓰고, 총액은 원 포맷을 사용하고 있어 UX가 혼란스럽습니다. 동일한 원화 포맷으로 맞춰주세요.
🔧 제안 수정안
+const formatWon = (price: number) => `${price.toLocaleString("ko-KR")}원`;
@@
- <p className="mt-1 text-[20px] font-extrabold leading-tight text-slate-800">
- ${item.price}
- </p>
+ <p className="mt-1 text-[20px] font-extrabold leading-tight text-slate-800">
+ {formatWon(item.price)}
+ </p>🤖 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 `@Week09/Mission02/src/components/CartItem.tsx` around lines 27 - 29, The
CartItem component is rendering item.price with a leading "$" which conflicts
with the total's Korean won formatting; update the CartItem.tsx rendering in the
CartItem component to format item.price using the same Korean won formatter used
for the total (e.g., Intl.NumberFormat('ko-KR', { style: 'currency', currency:
'KRW' }) or the project's currency helper) so the list rows display prices in 원
and match the total; locate the JSX that currently shows "${item.price}" and
replace it with the shared KRW formatting call for item.price.
| increase: (state, action: PayloadAction<string>) => { | ||
| const item = state.cartItems.find((cartItem) => cartItem.id === action.payload); | ||
|
|
||
| if (item) { | ||
| item.amount += 1; | ||
| } | ||
| }, | ||
| decrease: (state, action: PayloadAction<string>) => { | ||
| const item = state.cartItems.find((cartItem) => cartItem.id === action.payload); | ||
|
|
||
| if (!item) return; | ||
|
|
||
| if (item.amount === 1) { | ||
| state.cartItems = state.cartItems.filter( | ||
| (cartItem) => cartItem.id !== action.payload, | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| item.amount -= 1; | ||
| }, | ||
| removeItem: (state, action: PayloadAction<string>) => { | ||
| state.cartItems = state.cartItems.filter( | ||
| (cartItem) => cartItem.id !== action.payload, | ||
| ); | ||
| }, |
There was a problem hiding this comment.
파생 상태(amount, total)가 액션 직후 일시적으로 불일치합니다.
increase/decrease/removeItem에서 cartItems만 바꾸고 합계를 갱신하지 않아, 별도 calculateTotals가 실행되기 전까지 상태가 stale 할 수 있습니다. 합계 갱신을 reducer 내부에서 같이 처리해 상태 일관성을 보장하는 편이 안전합니다.
🔧 제안 수정안
interface CartState {
cartItems: CartItem[];
amount: number;
total: number;
}
@@
const initialState: CartState = {
cartItems,
amount: initialTotals.amount,
total: initialTotals.total,
};
+
+function syncTotals(state: CartState) {
+ const totals = getTotals(state.cartItems);
+ state.amount = totals.amount;
+ state.total = totals.total;
+}
@@
increase: (state, action: PayloadAction<string>) => {
const item = state.cartItems.find((cartItem) => cartItem.id === action.payload);
if (item) {
item.amount += 1;
+ syncTotals(state);
}
},
@@
if (item.amount === 1) {
state.cartItems = state.cartItems.filter(
(cartItem) => cartItem.id !== action.payload,
);
+ syncTotals(state);
return;
}
item.amount -= 1;
+ syncTotals(state);
},
removeItem: (state, action: PayloadAction<string>) => {
state.cartItems = state.cartItems.filter(
(cartItem) => cartItem.id !== action.payload,
);
+ syncTotals(state);
},🤖 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 `@Week09/Mission02/src/features/cart/cartSlice.ts` around lines 36 - 61, The
reducers increase, decrease, and removeItem update cartItems but do not update
derived totals (amount and total), causing a temporary state mismatch; fix by
ensuring each of these reducers also updates state.amount and state.total
immediately after modifying cartItems (either by invoking the existing
calculateTotals reducer logic directly like calculateTotals(state) or by
recomputing amount/total inline) so the store remains consistent; update the
implementations of increase, decrease, and removeItem to recalc totals right
after changing cartItems.
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>Week09 Mission1</title> |
There was a problem hiding this comment.
문서 제목이 현재 미션과 불일치합니다.
<title>이 Week09 Mission1로 되어 있어 Mission03 프로젝트와 맞지 않습니다. 브라우저 탭/북마크에서 혼동을 줄 수 있으니 현재 미션명으로 맞춰주세요.
🤖 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 `@Week09/Mission03/index.html` at line 6, HTML 문서의 <title> 요소 값이 "Week09
Mission1"으로 잘못되어 현재 프로젝트와 불일치합니다; index.html의 <title> 내용을 현재 미션명(예: "Week09
Mission03" 또는 프로젝트명)으로 변경하여 브라우저 탭과 북마크에 맞게 정확한 미션명을 반영하세요.
| {item.singer} | ||
| </p> | ||
| <p className="mt-1 text-[20px] font-extrabold leading-tight text-slate-800"> | ||
| ${item.price} |
There was a problem hiding this comment.
통화 표기가 총액 표시와 불일치합니다.
Line 27은 $를 사용하지만 화면의 합계/문구는 원 기준이라 사용자에게 혼동을 줄 수 있습니다.
제안 diff
- <p className="mt-1 text-[20px] font-extrabold leading-tight text-slate-800">
- ${item.price}
- </p>
+ <p className="mt-1 text-[20px] font-extrabold leading-tight text-slate-800">
+ {Number(item.price).toLocaleString("ko-KR")}원
+ </p>📝 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.
| ${item.price} | |
| <p className="mt-1 text-[20px] font-extrabold leading-tight text-slate-800"> | |
| {Number(item.price).toLocaleString("ko-KR")}원 | |
| </p> |
🤖 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 `@Week09/Mission03/src/components/CartItem.tsx` at line 27, The currency symbol
in the CartItem component is inconsistent: replace the hardcoded "$" at the
interpolation of item.price in CartItem.tsx with the same currency/format used
elsewhere (Korean won) or use a centralized formatter; update the interpolation
that currently reads `${item.price}` to render with "원" (e.g., `${item.price}원`)
or call the shared formatter function (e.g., formatCurrency(item.price)) so the
displayed unit matches the cart total and app-wide currency.
| <p className={`error-message ${state.error ? "visible" : ""}`}> | ||
| {state.error} | ||
| </p> | ||
|
|
||
| <div className="form-row"> | ||
| <input | ||
| type="text" | ||
| value={department} | ||
| onChange={handleChange} | ||
| placeholder="카드메이커" | ||
| className="department-input" | ||
| /> |
There was a problem hiding this comment.
접근성: 입력 필드 라벨과 오류 알림 라이브 리전을 추가해주세요.
현재 입력 목적이 명시적으로 연결되지 않고, 오류 변경도 보조기기에 즉시 공지되지 않습니다. 스크린리더 사용자의 입력/수정 흐름이 끊길 수 있습니다.
수정 예시
- <p className={`error-message ${state.error ? "visible" : ""}`}>
+ <p
+ className={`error-message ${state.error ? "visible" : ""}`}
+ role="alert"
+ aria-live="assertive"
+ >
{state.error}
</p>
<div className="form-row">
+ <label htmlFor="department-input" className="sr-only">
+ 직무 입력
+ </label>
<input
+ id="department-input"
type="text"
value={department}
onChange={handleChange}
placeholder="카드메이커"
className="department-input"
/>🤖 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 `@Week09/Practice01/src/components/UseReducerCompany.tsx` around lines 59 - 70,
Add accessible labeling and a live error region: give the text input (currently
bound to the department state via value={department} and
onChange={handleChange}) a visible label element tied with htmlFor/id, mark the
input with aria-invalid={Boolean(state.error)} and add aria-describedby that
points to the error message id; convert the error <p> (which currently renders
{state.error}) into a role="alert" or aria-live="assertive" region with a stable
id so screen readers are notified when state.error changes. Ensure identifiers
referenced (department input, handleChange, state.error, and the error <p>) are
updated consistently.
wantkdd
left a comment
There was a problem hiding this comment.
Redux Toolkit과 Zustand 전환까지 9주차 미션 흐름은 전반적으로 잘 구현되어 있는데, 현재 파일들이 Week09/Mission01처럼 공용 경로에 바로 올라와 있어서 이후 다른 제출물과 경로 충돌이 날 수 있으니 다음 제출부터는 닉네임/이름 폴더로 한 번 감싸두면 더 안전할 것 같습니다. Zustand 버전에서는 usePlaylistStore()를 통째로 구독하는 컴포넌트가 많아 amount, total, modal 상태 변화에도 장바구니 아이템 쪽까지 같이 리렌더될 수 있으므로, 필요한 값별 selector로 나누면 이번 주차의 부분 구독 의도까지 더 잘 살아날 것 같습니다. 전체 기능 흐름은 잘 갖춰져 있으니 이 두 부분만 다음 개선 때 챙겨보면 좋겠습니다, 수고하셨습니다!
📝 미션 번호
9주차 practice01
📋 구현 사항
📎 스크린샷
📝 미션 번호
9주차 Misson 1
📋 구현 사항
📎 스크린샷
📝 미션 번호
9주차 Misson 2
📋 구현 사항
📎 스크린샷
📝 미션 번호
9주차 Misson 3
📋 구현 사항
📎 스크린샷
✅ 체크리스트