Skip to content

Merge refactor-skeleton โ†’ refacgtor#311

Merged
ljh130334 merged 44 commits into
refactorfrom
refactor-skeleton
Feb 25, 2026
Merged

Merge refactor-skeleton โ†’ refacgtor#311
ljh130334 merged 44 commits into
refactorfrom
refactor-skeleton

Conversation

@ljh130334

@ljh130334 ljh130334 commented Feb 25, 2026

Copy link
Copy Markdown
Member

์ดํ•˜๋™์ผ

Summary by CodeRabbit

  • ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ

    • ์ „์—ญ ์Šค์ผˆ๋ ˆํ†ค ๋กœ๋”ฉ ์ปดํฌ๋„ŒํŠธ ๋Œ€๊ฑฐ ์ถ”๊ฐ€ ๋ฐ ํ†ตํ•ฉ ์ œ๊ณต (ํ”ผ๋“œ/๊ทธ๋ฃน/๊ฒ€์ƒ‰/์ฑ…/๋ฉ”๋ชจ๋ฆฌ/๋ฉค๋ฒ„/๋ฉ”์‹œ์ง€ ๋“ฑ)
  • ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๊ฐœ์„ 

    • ํŽ˜์ด์ง€๋ณ„ ๊ตฌ์กฐํ™”๋œ ์Šค์ผˆ๋ ˆํ†ค์œผ๋กœ ๋นˆ ํ™”๋ฉด ์ œ๊ฑฐ ๋ฐ ๋ ˆ์ด์•„์›ƒ ์œ ์ง€
    • ํƒญ/์ƒ์„ธ/์ดˆ๊ธฐ ๋กœ๋”ฉ์— ์ตœ์†Œ 500ms ํ‘œ์‹œ ๋ณด์žฅ์œผ๋กœ ๊นœ๋ฐ•์ž„ ์™„ํ™”
    • ๊ธฐ์กด ์Šคํ”ผ๋„ˆ๋ฅผ ๋Œ€์ฒดํ•œ ์‹œ๊ฐ์  ํ”Œ๋ ˆ์ด์Šคํ™€๋”์™€ ๊ฐœ์„ ๋œ ์˜ค๋ฅ˜/์ ‘๊ทผ์„ฑ ์ฒ˜๋ฆฌ
  • ๋ฌธ์„œํ™”

    • ์Šค์ผˆ๋ ˆํ†ค ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉ ๊ฐ€์ด๋“œ ๋ฐ ์˜ˆ์ œ ๋ฌธ์„œ ์ถ”๊ฐ€

- LoadingSpinner ๋Œ€์‹  ์ฝ˜ํ…์ธ  ๊ตฌ์กฐ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” Skeleton UI ๋„์ž…
- shared/ui/Skeleton์— ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„ (Box/Circle/Text)
- ํ”ผ๋“œ ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์ƒํƒœ์— ์Šค์ผˆ๋ ˆํ†ค ์ ์šฉ์œผ๋กœ UX ๊ฐœ์„ 
THIP-405: Skeleton UI ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„
- ๊ธฐ์กด ๋นˆ Fragment(A) โ†’ FeedPostSkeleton 3๊ฐœ ํ‘œ์‹œ(B)๋กœ ๋กœ๋”ฉ UX ๊ฐœ์„ 
- ํ—ค๋”/NavBar ์œ ์ง€ํ•œ ์ฑ„๋กœ ์Šค์ผˆ๋ ˆํ†ค ์˜์—ญ๋งŒ ๊ต์ฒด
- ์ตœ์†Œ 0.5์ดˆ ๋กœ๋”ฉ ๋ณด์žฅ์œผ๋กœ ๊นœ๋นก์ž„ ๋ฐฉ์ง€
- OtherFeedSkeleton ์‹ ๊ทœ ์ƒ์„ฑ: ํ”„๋กœํ•„+ํŒ”๋กœ์›Œ+๊ฒŒ์‹œ๋ฌผ ์˜์—ญ ํฌํ•จ, paddingTop prop์œผ๋กœ ํ—ค๋” ๋†’์ด ๋Œ€์‘
- Feed ๋‚ด ํ”ผ๋“œ ํƒญ: FeedPostSkeleton โ†’ OtherFeedSkeleton(paddingTop=136)์œผ๋กœ ๊ต์ฒด
- MyFeed ํ”„๋กœํ•„ ๋กœ๋”ฉ ์ค‘ ๋นˆ ํ™”๋ฉด(<>) โ†’ OtherFeedSkeleton์œผ๋กœ ๊ต์ฒด
- MyFeedPage: getOtherProfile โ†’ getMyProfile ๊ต์ฒด๋กœ ํ”„๋กœํ•„ ๋ฏธ๋…ธ์ถœ ๋ฒ„๊ทธ ์ˆ˜์ •
- A: ํ‰ํƒ„ํ•œ ๊ตฌ์กฐ(9๊ฐœ ํŒŒ์ผ ํ˜ผ์žฌ), Profile/TotalBar ๋กœ์ง์ด OtherFeedSkeleton์—๋งŒ ๊ฒฐํ•ฉ
- B: base/feed ๋„๋ฉ”์ธ ๋ถ„๋ฆฌ + ProfileSkeleton/TotalBarSkeleton ๋…๋ฆฝ ์ถ”์ถœ โ†’ ์กฐํ•ฉํ˜• ์„ค๊ณ„
- ์žฌ์‚ฌ์šฉ์„ฑ: Profile/TotalBar ์Šค์ผˆ๋ ˆํ†ค ๋…๋ฆฝ ์‚ฌ์šฉ ๊ฐ€๋Šฅ, import ๊ฒฝ๋กœ ๊ฐ„์†Œํ™” (๊ฐœ๋ณ„ ๊ฒฝ๋กœ โ†’ '@/shared/ui/Skeleton')
- A: ๋กœ๋”ฉ ์ค‘ ๋นˆ ํ™”๋ฉด(<>) ํ‘œ์‹œ
- B: FeedPostSkeleton + ๋Œ“๊ธ€ ์Šค์ผˆ๋ ˆํ†ค(Circle+Text) ร— 5 ํ‘œ์‹œ
- ์ตœ์†Œ 0.5์ดˆ ๋กœ๋”ฉ์œผ๋กœ ๊นœ๋นก์ž„ ๋ฐฉ์ง€, ๋ ˆ์ด์•„์›ƒ ์‚ฌ์ „ ๋…ธ์ถœ๋กœ CLS ๊ฐœ์„ 
- A๋Š” ๋ฒ”์šฉ LoadingSpinner โ†’ ํƒญ ์ „ํ™˜ ์‹œ ์ฝ˜ํ…์ธ  ๊ตฌ์กฐ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ถˆ๊ฐ€
- B๋Š” ํƒญ๋ณ„ ์Šค์ผˆ๋ ˆํ†ค(ํ”ผ๋“œ: FeedPostSkeleton, ์ฑ…: BookSkeletonItem) โ†’ ๋ ˆ์ด์•„์›ƒ ๋ฏธ๋ฆฌ ํ‘œ์‹œ
- ์ตœ์†Œ 500ms ๋กœ๋”ฉ์œผ๋กœ ๊นœ๋นก์ž„ ๋ฐฉ์ง€
- A๋Š” LoadingSpinner๋กœ ์ „์ฒด ํŽ˜์ด์ง€ ๋กœ๋”ฉ ํ‘œ์‹œ โ†’ ๋ฉ”๋‰ด ๊ตฌ์กฐ ํ™•์ธ ๋ถˆ๊ฐ€
- B๋Š” ํ”„๋กœํ•„๋งŒ ์Šค์ผˆ๋ ˆํ†ค, ๊ณ ์ • ๋ฉ”๋‰ด๋Š” ์ฆ‰์‹œ ํ‘œ์‹œ โ†’ ๋กœ๋”ฉ ์ค‘์—๋„ ํŽ˜์ด์ง€ ํƒ์ƒ‰ ๊ฐ€๋Šฅ
- ์ตœ์†Œ 500ms ๋กœ๋”ฉ์œผ๋กœ ํ”„๋กœํ•„ ์Šค์ผˆ๋ ˆํ†ค ๊นœ๋นก์ž„ ๋ฐฉ์ง€
- A๋Š” LoadingSpinner ์ค‘์•™ ํ‘œ์‹œ โ†’ ๋ฉ”์‹œ์ง€ ๊ตฌ์กฐ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ถˆ๊ฐ€
- B๋Š” ๋‚ ์งœ ๊ตฌ๋ถ„์„  + ๋ฉ”์‹œ์ง€ 4๊ฐœ ์Šค์ผˆ๋ ˆํ†ค โ†’ ์‹ค์ œ ๋ฉ”์‹œ์ง€ ๋ ˆ์ด์•„์›ƒ ๋ฏธ๋ฆฌ ํ‘œ์‹œ
- ์ตœ์†Œ 500ms ๋กœ๋”ฉ์œผ๋กœ ์ดˆ๊ธฐ ๋กœ๋“œ ์‹œ ๊นœ๋นก์ž„ ๋ฐฉ์ง€
- ์ „์ฒด ํŽ˜์ด์ง€ ๋กœ๋”ฉ(A) โ†’ ๊ณ ์ • UI ์ฆ‰์‹œ ํ‘œ์‹œ + ์ฝ˜ํ…์ธ ๋งŒ ์Šค์ผˆ๋ ˆํ†ค(B)
- BannerSkeleton/BookSkeleton ๋ถ„๋ฆฌ๋กœ ์„น์…˜๋ณ„ ๋…๋ฆฝ ์ œ์–ด
- ๊นœ๋นก์ž„ ๋ฐฉ์ง€ ์œ„ํ•ด ์ตœ์†Œ 500ms ๋กœ๋”ฉ + null ์•ˆ์ „์„ฑ ๊ฐ•ํ™”
- ์ „์ฒด ๋กœ๋”ฉ(A) โ†’ ๊ณ ์ • UI + ์ฝ˜ํ…์ธ  ์Šค์ผˆ๋ ˆํ†ค(B)์œผ๋กœ ๊ฐœ์„ 
- BannerSkeleton์œผ๋กœ ๋ฐฐ๋„ˆ ์˜์—ญ, ์ธ๋ผ์ธ ์Šค์ผˆ๋ ˆํ†ค์œผ๋กœ ์ฑ… ์ •๋ณด ํ‘œ์‹œ
- ์ตœ์†Œ 500ms ๋กœ๋”ฉ์œผ๋กœ ๊นœ๋นก์ž„ ๋ฐฉ์ง€, IIFE๋กœ ํƒ€์ž… ์•ˆ์ „์„ฑ ํ™•๋ณด
- ๋กœ๋”ฉ ์—†์Œ(A) โ†’ ํƒญ/ํ•„ํ„ฐ ์œ ์ง€ + RecordItem ์Šค์ผˆ๋ ˆํ†ค(B)
- RecordItemSkeleton์œผ๋กœ ํ”„๋กœํ•„/์ฝ˜ํ…์ธ /์•ก์…˜ ์˜์—ญ ๊ตฌ์กฐ ์œ ์ง€
- ์ตœ์†Œ 500ms ๋กœ๋”ฉ์œผ๋กœ ๊นœ๋นก์ž„ ๋ฐฉ์ง€, ํƒญ/ํ•„ํ„ฐ ์กฐ์ž‘ ๊ฐ€๋Šฅ ์ƒํƒœ ์œ ์ง€
- A: LoadingSpinner/ํ…์ŠคํŠธ๋กœ ๋กœ๋”ฉ ํ‘œ์‹œ โ†’ ๋ ˆ์ด์•„์›ƒ ์ ํ”„ ๋ฐœ์ƒ
- B: ์‹ค์ œ ์ปดํฌ๋„ŒํŠธ ๊ตฌ์กฐ ๊ธฐ๋ฐ˜ ์Šค์ผˆ๋ ˆํ†ค + 500ms ์ตœ์†Œ ๋กœ๋”ฉ โ†’ ๋ ˆ์ด์•„์›ƒ ์•ˆ์ •์„ฑ
- Memory 4๊ฐœ, SearchBook ํ”ผ๋“œ 3๊ฐœ ์Šค์ผˆ๋ ˆํ†ค์œผ๋กœ ๊นœ๋นก์ž„ ๋ฐฉ์ง€ ๋ฐ UX ๊ฐœ์„ 
- A: LoadingSpinner/ํ…์ŠคํŠธ ๋กœ๋”ฉ ํ‘œ์‹œ โ†’ ๋ ˆ์ด์•„์›ƒ ์ ํ”„ ๋ฐ ๊นœ๋นก์ž„ ๋ฐœ์ƒ
- B: ์‹ค์ œ ์ปดํฌ๋„ŒํŠธ ๊ตฌ์กฐ ๊ธฐ๋ฐ˜ ์Šค์ผˆ๋ ˆํ†ค + 500ms ์ตœ์†Œ ๋กœ๋”ฉ โ†’ ์•ˆ์ •์  UX
- Memory 3๊ฐœ, Search 5๊ฐœ, SearchBook ํ”ผ๋“œ 3๊ฐœ ์Šค์ผˆ๋ ˆํ†ค์œผ๋กœ ์ผ๊ด€๋œ ๋กœ๋”ฉ ๊ฒฝํ—˜ ์ œ๊ณต
- ๊ธฐ์กด: LoadingMessage ํ…์ŠคํŠธ๋กœ ๋กœ๋”ฉ ํ‘œ์‹œ
- ๊ฐœ์„ : ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋Š” GroupCardSkeleton(5๊ฐœ), ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด๋Š” RecentSearchTabsSkeleton์œผ๋กœ ๊ตฌ์กฐ ์ผ์น˜
- 500ms ์ตœ์†Œ ๋กœ๋”ฉ์œผ๋กœ ๊นœ๋นก์ž„ ๋ฐฉ์ง€ ๋ฐ ๋กœ๋”ฉ ์ƒํƒœ ์ผ๊ด€์„ฑ ํ™•๋ณด
- ๊ธฐ์กด: ๋กœ๋”ฉ ์ค‘... ํ…์ŠคํŠธ๋กœ ํ‘œ์‹œ
- ๊ฐœ์„ : MemberListSkeleton(5๊ฐœ)๋กœ ์‹ค์ œ ๋ฉค๋ฒ„ ๋ฆฌ์ŠคํŠธ ๊ตฌ์กฐ์™€ ๋™์ผํ•˜๊ฒŒ ํ‘œ์‹œ
- 500ms ์ตœ์†Œ ๋กœ๋”ฉ์œผ๋กœ ๊นœ๋นก์ž„ ๋ฐฉ์ง€ ๋ฐ ์ผ๊ด€๋œ ๋กœ๋”ฉ ๊ฒฝํ—˜ ์ œ๊ณต
- ๊ธฐ์กด: LoadingSpinner๋กœ ์ „์ฒด ํ™”๋ฉด ๋กœ๋”ฉ ํ‘œ์‹œ
- ๊ฐœ์„ : UserProfileItemSkeleton(5๊ฐœ)๋กœ ์‹ค์ œ ํ”„๋กœํ•„ ๋ฆฌ์ŠคํŠธ ๊ตฌ์กฐ์™€ ๋™์ผํ•˜๊ฒŒ ํ‘œ์‹œ
- ์ดˆ๊ธฐ ๋กœ๋”ฉ ์‹œ 500ms ์ตœ์†Œ ๋กœ๋”ฉ์œผ๋กœ ๊นœ๋นก์ž„ ๋ฐฉ์ง€ ๋ฐ ์ผ๊ด€๋œ UX ์ œ๊ณต
- getMyProfile()(๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž)๊ณผ getOtherFeed(userId)(URL ๊ธฐ์ค€) ํ˜ผ์šฉ์œผ๋กœ ๋ฐ์ดํ„ฐ ๋ถˆ์ผ์น˜ ๋ฐœ์ƒ
- getOtherProfile(userId)๋กœ ๊ต์ฒดํ•˜์—ฌ ํ”ผ๋“œยทํ”„๋กœํ•„ ๋ชจ๋‘ URL userId ๊ธฐ์ค€์œผ๋กœ ํ†ต์ผ
- isFollowing ์ˆ˜๋™ ์ฃผ์ž… ์ œ๊ฑฐ, API ์‘๋‹ต๊ฐ’ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ
- ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ Header๊ฐ€ ์—†์–ด ๋ธŒ๋ผ์šฐ์ € ๋’ค๋กœ๊ฐ€๊ธฐ ์™ธ ํƒˆ์ถœ ์ˆ˜๋‹จ ์—†์Œ
- ์—๋Ÿฌ ์ƒํƒœ์—์„œ๋„ TitleHeader ๋ Œ๋”๋งํ•˜์—ฌ ๋’ค๋กœ๊ฐ€๊ธฐ ๋ฒ„ํŠผ ์ œ๊ณต
- ๋ชจ๋ฐ”์ผ ํ™˜๊ฒฝ์—์„œ ์‚ฌ์šฉ์ž๊ฐ€ ํ™”๋ฉด์— ๊ฐ‡ํžˆ๋Š” UX ๋ฌธ์ œ ํ•ด๊ฒฐ
- ๋กœ๋”ฉ ์ค‘ RecordTabsยทRecordFilters ํด๋ฆญ ์‹œ 2์ฐจ ์š”์ฒญ ๋ฐœ์ƒ โ†’ stale ๋ฐ์ดํ„ฐ ๋…ธ์ถœ ๊ฐ€๋Šฅ
- FixedSection์— pointerEvents: none ์ ์šฉ์œผ๋กœ ๋กœ๋”ฉ ์ค‘ ์ธํ„ฐ๋ž™์…˜ ์ฐจ๋‹จ
- AbortController ์—†์ด ๋‹จ์ˆœํ•œ ๋ฐฉ์–ด ์ฒ˜๋ฆฌ๋กœ Race Condition ๋ฐฉ์ง€
- MenuItemSkeleton์ด ์–ด๋””์—๋„ ์‚ฌ์šฉ๋˜์ง€ ์•Š์•„ dead code๋กœ ์ œ๊ฑฐ
- ํ”„๋กœํ•„ img์— alt ์—†์–ด ์Šคํฌ๋ฆฐ ๋ฆฌ๋”๊ฐ€ ์ด๋ฏธ์ง€ ๋ชฉ์  ์ธ์‹ ๋ถˆ๊ฐ€
- profile.nickname ๊ธฐ๋ฐ˜ alt ํ…์ŠคํŠธ ์ถ”๊ฐ€๋กœ ์ ‘๊ทผ์„ฑ ๊ฐœ์„ 
- A๋Š” isLoading || ์กฐ๊ฑด์„ JSX ๋‚ด๋ถ€ ์‚ผํ•ญ์œผ๋กœ ๋ถ„์‚ฐ ์ฒ˜๋ฆฌ โ†’ ๊ฐ€๋…์„ฑ ์ €ํ•˜
- B๋Š” ํ•จ์ˆ˜ ์ƒ๋‹จ early return์œผ๋กœ skeleton ๋ฐ˜ํ™˜ ํ›„ data non-null ๋ณด์žฅ
- GroupDetail: BannerSkeleton/BookSkeleton ๋ถ„๊ธฐ ์ œ๊ฑฐ, buttonPropsยทhandlers ๋‹จ์ˆœํ™”
- ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ <div>๋งŒ ๋ฐ˜ํ™˜ํ•ด ์‚ฌ์šฉ์ž๊ฐ€ ๋ธŒ๋ผ์šฐ์ € ๋’ค๋กœ๊ฐ€๊ธฐ ์™ธ ํƒˆ์ถœ ๋ถˆ๊ฐ€
- ParticipatedGroupDetail ์—๋Ÿฌ ์ƒํƒœ TitleHeader ์ถ”๊ฐ€์™€ ๋™์ผํ•œ ์ด์Šˆ
- Wrapper + Header + IconButton ๊ตฌ์กฐ๋กœ ์—๋Ÿฌ ํ™”๋ฉด์—๋„ ๋’ค๋กœ๊ฐ€๊ธฐ ์ œ๊ณต
refactor-skeleton-global: ์ฃผ์š” ํŽ˜์ด์ง€ ์ „์—ญ Skeleton UI ์ ์šฉ ๋ฐ ๋กœ๋”ฉ ๊ฒฝํ—˜ ๊ฐœ์„ 
@ljh130334 ljh130334 self-assigned this Feb 25, 2026
@vercel

vercel Bot commented Feb 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
thip Ready Ready Preview, Comment Feb 25, 2026 10:18am

@coderabbitai

coderabbitai Bot commented Feb 25, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@ljh130334 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 4 minutes and 25 seconds before requesting another review.

โŒ› How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

๐Ÿšฆ How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between f38029c and 2a3be9f.

๐Ÿ“’ Files selected for processing (5)
  • src/pages/searchBook/SearchBook.tsx
  • src/shared/ui/Skeleton/README.md
  • src/shared/ui/Skeleton/base/Skeleton.tsx
  • src/shared/ui/Skeleton/group/GroupBookSectionSkeleton.tsx
  • src/shared/ui/Skeleton/searchBook/BookDetailSkeleton.tsx

Walkthrough

์ „์—ญ์ ์œผ๋กœ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ Skeleton ์ปดํฌ๋„ŒํŠธ๊ตฐ์„ ์ถ”๊ฐ€ํ•˜๊ณ , ์—ฌ๋Ÿฌ ํŽ˜์ด์ง€/์ปดํฌ๋„ŒํŠธ์˜ ๊ธฐ์กด ์Šคํ”ผ๋„ˆ/ํ…์ŠคํŠธ ๋กœ๋”๋ฅผ ์Šค์ผˆ๋ ˆํ†ค UI๋กœ ๊ต์ฒดํ•˜๋ฉฐ ํƒญ/์ดˆ๊ธฐ ๋กœ๋”ฉ์— 500ms ์ตœ์†Œ ์ง€์—ฐ์„ Promise.all๋กœ ๋ณ‘๋ ฌ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.

Changes

Cohort / File(s) Summary
Skeleton core
src/shared/ui/Skeleton/base/Skeleton.styled.ts, src/shared/ui/Skeleton/base/Skeleton.tsx, src/shared/ui/Skeleton/base/index.ts
๊ธฐ๋ณธ Skeleton(Box/Circle/Text) ์Šคํƒ€์ผยท์ปดํฌ๋„ŒํŠธ ๋ฐ ์‹œ๋จธ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ถ”๊ฐ€, ์žฌ๋‚ด๋ณด๋‚ด๊ธฐ ๊ตฌํ˜„.
Skeleton barrels & docs
src/shared/ui/Skeleton/index.ts, src/shared/ui/Skeleton/README.md
Skeleton ๊ตฌ์„ฑ์š”์†Œ ์ค‘์•™ ์žฌ๋‚ด๋ณด๋‚ด๊ธฐ(๋ฐ”๋ )์™€ ์‚ฌ์šฉ ๋ฌธ์„œ ์ถ”๊ฐ€.
Feed skeletons & feed pages
src/shared/ui/Skeleton/feed/*, src/shared/ui/Skeleton/feed/index.ts, src/components/feed/MyFeed.tsx, src/pages/feed/Feed.tsx, src/pages/feed/Feed.styled.ts, src/pages/feed/FeedDetailPage.tsx, src/pages/feed/FeedDetailPage.styled.ts, src/pages/feed/MyFeedPage.tsx, src/pages/feed/OtherFeedPage.tsx, src/pages/feed/FollowerListPage.tsx
Feed ๊ด€๋ จ ์Šค์ผˆ๋ ˆํ†ค ์ปดํฌ๋„ŒํŠธ(FeedPost, OtherFeed, Profile, TotalBar, UserProfileItem ๋“ฑ) ์ถ”๊ฐ€ ๋ฐ ์—ฌ๋Ÿฌ ํ”ผ๋“œ ํŽ˜์ด์ง€์—์„œ ๋กœ๋”ฉ ๊ฒฝ๋กœ๋ฅผ ์Šค์ผˆ๋ ˆํ†ค+minLoadingTime(500ms)์œผ๋กœ ์ „ํ™˜.
Group skeletons & group pages
src/shared/ui/Skeleton/group/*, src/shared/ui/Skeleton/group/index.ts, src/pages/groupDetail/GroupDetail.tsx, src/pages/groupDetail/ParticipatedGroupDetail.tsx, src/pages/groupMembers/GroupMembers.tsx
๊ทธ๋ฃน ์ƒ์„ธ/์ฐธ์—ฌ ํŽ˜์ด์ง€์šฉ ์Šค์ผˆ๋ ˆํ†ค ์ถ”๊ฐ€, ๋กœ๋”ฉ ๋ถ„๊ธฐ ๋Œ€์ฒด, ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ๊ฒฝ๋กœ๋ฅผ roomData ๊ธฐ๋ฐ˜์œผ๋กœ ์ •๋ฆฌ ๋ฐ 500ms ์ตœ์†Œ ์ง€์—ฐ ์ ์šฉ.
Search & SearchBook skeletons
src/shared/ui/Skeleton/search/*, src/shared/ui/Skeleton/searchBook/*, src/pages/search/Search.tsx, src/pages/searchBook/SearchBook.tsx, src/components/search/MostSearchedBooks.tsx, src/pages/groupSearch/GroupSearch.tsx
๊ฒ€์ƒ‰ ์˜์—ญ(์ตœ๊ทผ๊ฒ€์ƒ‰, ์ธ๊ธฐ ๋„์„œ, ๋ถ ์ƒ์„ธ)์— ์Šค์ผˆ๋ ˆํ†ค ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€, ๋กœ๋”ฉ ๋ถ„๊ธฐ์—์„œ ์Šค์ผˆ๋ ˆํ†ค ์‚ฌ์šฉ ๋ฐ 500ms ์ตœ์†Œ ์ง€์—ฐ ๋„์ž….
Memory / TodayWords / Members / Mypage / Save skeletons & pages
src/shared/ui/Skeleton/memory/*, src/shared/ui/Skeleton/todaywords/*, src/shared/ui/Skeleton/members/*, src/pages/memory/Memory.tsx, src/pages/today-words/TodayWords.tsx, src/pages/groupMembers/GroupMembers.tsx, src/pages/mypage/*, src/pages/mypage/SavePage.*, src/pages/mypage/Mypage.tsx, src/pages/mypage/SavePage.tsx
๋ฉ”๋ชจ๋ฆฌยท์˜ค๋Š˜์˜ํ•œ๋งˆ๋””ยท๋ฉค๋ฒ„ยท๋งˆ์ดํŽ˜์ด์ง€ยท์ €์žฅ ํŽ˜์ด์ง€์— ๋Œ€์‘ํ•˜๋Š” ์Šค์ผˆ๋ ˆํ†ค ์ถ”๊ฐ€ ๋ฐ ์ดˆ๊ธฐ ๋กœ๋”ฉ์—์„œ ์Šค์ผˆ๋ ˆํ†ค+minLoadingTime ๋ณ‘๋ ฌ ์ ์šฉ.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Page as PageComponent
    participant API as RemoteAPI
    participant Delay as MinLoading(500ms)
    participant Skeleton as SkeletonUI
    participant Content as FinalContent

    User->>Page: ์ง„์ž… ๋˜๋Š” ํƒญ ์ „ํ™˜
    Page->>Page: isLoading = true
    Page->>Skeleton: ์Šค์ผˆ๋ ˆํ†ค ๋ Œ๋”๋ง (์ดˆ๊ธฐ ๋ทฐ)
    par ๋ณ‘๋ ฌ ์‹คํ–‰
        Page->>API: ๋ฐ์ดํ„ฐ ์š”์ฒญ (fetch)
        Page->>Delay: 500ms ํƒ€์ด๋จธ ์‹œ์ž‘
    end
    API-->>Page: ๋ฐ์ดํ„ฐ ์‘๋‹ต
    Delay-->>Page: 500ms ๋งŒ๋ฃŒ
    Page->>Page: isLoading = false
    Page->>Content: ์‹ค์ œ ์ฝ˜ํ…์ธ  ๋ Œ๋”๋ง
    Content-->>User: ํ™”๋ฉด ํ‘œ์‹œ
Loading

Estimated code review effort

๐ŸŽฏ 4 (Complex) | โฑ๏ธ ~60 minutes

Possibly related PRs

Suggested reviewers

  • heeeeyong
  • ho0010

Poem

๐Ÿ‡ ์Šค์ผˆ๋ ˆํ†ค ์‚ด๊ธˆ์‚ด๊ธˆ ์™€์„œ,
๋ฐ˜์ดˆ(500ms) ๋™์•ˆ ์ถค์„ ์ถ”๋ฉฐ ๋น›๋‚˜๋„ค,
์‹œ๋จธ ๋น›๊น”์— ์ˆจ ๊ณ ๋ฅด๊ณ  ๊ธฐ๋‹ค๋ฆฌ๋ฉด,
ํฌ์ŠคํŠธ์™€ ๋ฐฉ(๊ทธ๋ฃน)์ด ์‚ด๋ฉฐ์‹œ ๋ชจ์Šต์„ ๋น„์ถ”๋„ค.
ํ† ๋ผ๋Š” ๊นก์ถฉโ€”๋ณ€๊ฒฝ ์™„๋ฃŒ, ํ›Œ๋ฅญํ•ด์š”!

๐Ÿšฅ Pre-merge checks | โœ… 1 | โŒ 2

โŒ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage โš ๏ธ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check โ“ Inconclusive PR ์ œ๋ชฉ 'Merge refactor-skeleton โ†’ refacgtor'๋Š” ๋ธŒ๋žœ์น˜ ๋ณ‘ํ•ฉ ์ž‘์—…์„ ์„ค๋ช…ํ•˜์ง€๋งŒ, ์‹ค์ œ ๋ณ€๊ฒฝ์˜ ํ•ต์‹ฌ ๋ชฉ์ ์ธ ์Šค์ผˆ๋ ˆํ†ค UI ์‹œ์Šคํ…œ ๊ตฌํ˜„๊ณผ ๋กœ๋”ฉ ์ƒํƒœ ๊ฐœ์„ ์„ ๋ช…ํ™•ํ•˜๊ฒŒ ์ „๋‹ฌํ•˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค. PR ์ œ๋ชฉ์„ '์Šค์ผˆ๋ ˆํ†ค UI ์‹œ์Šคํ…œ ๊ตฌํ˜„ ๋ฐ ๋กœ๋”ฉ ์ƒํƒœ ๊ฐœ์„ '๊ณผ ๊ฐ™์ด ๋ณ€๊ฒฝํ•˜์—ฌ ์‹ค์ œ ๋ณ€๊ฒฝ์˜ ์ฃผ์š” ๋ชฉ์ ์„ ๋ช…ํ™•ํžˆ ํ‘œํ˜„ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.
โœ… Passed checks (1 passed)
Check name Status Explanation
Description Check โœ… Passed Check skipped - CodeRabbitโ€™s high-level summary is enabled.

โœ๏ธ Tip: You can configure your own custom pre-merge checks in the settings.

โœจ Finishing Touches
๐Ÿงช Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor-skeleton

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.

โค๏ธ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 16

Caution

Some comments are outside the diff and canโ€™t be posted inline due to platform limitations.

โš ๏ธ Outside diff range comments (5)
src/pages/feed/MyFeedPage.tsx (1)

75-77: โš ๏ธ Potential issue | ๐ŸŸ  Major

์—๋Ÿฌ ์ƒํƒœ์—์„œ ๋นˆ ํ™”๋ฉด(<></>) ๋ฐ˜ํ™˜ โ€” ์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ ๋ฐ ๋’ค๋กœ๊ฐ€๊ธฐ ์ˆ˜๋‹จ ์—†์Œ

์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ๋นˆ ํ”„๋ž˜๊ทธ๋จผํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉด ์‚ฌ์šฉ์ž์—๊ฒŒ ์•„๋ฌด๋Ÿฐ ์•ˆ๋‚ด ์—†์ด ๋นˆ ํ™”๋ฉด๋งŒ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ๋’ค๋กœ๊ฐ€๊ธฐ ๋ฒ„ํŠผ์ด๋‚˜ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ์—†์ด๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ํŽ˜์ด์ง€๋ฅผ ๋ฒ—์–ด๋‚  ๋ฐฉ๋ฒ•์ด ์—†์Šต๋‹ˆ๋‹ค. PR ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€์—์„œ๋„ GroupDetail/ParticipatedGroupDetail์˜ ์—๋Ÿฌ ์ƒํƒœ์— ๋’ค๋กœ๊ฐ€๊ธฐ ๋„ค๋น„๊ฒŒ์ด์…˜์„ ์ถ”๊ฐ€ํ–ˆ๋‹ค๊ณ  ์–ธ๊ธ‰ํ•˜๊ณ  ์žˆ์–ด, ๋™์ผํ•œ ํŒจํ„ด์ด ์—ฌ๊ธฐ์—๋„ ์ ์šฉ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ› ์ˆ˜์ • ์ œ์•ˆ
-  if (error) {
-    return <></>;
-  }
+  if (error) {
+    return (
+      <Container>
+        <TitleHeader
+          leftIcon={<img src={leftArrow} alt="๋’ค๋กœ๊ฐ€๊ธฐ" />}
+          onLeftClick={handleBackClick}
+        />
+        <div style={{ padding: '20px', textAlign: 'center', color: 'gray' }}>
+          ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.
+        </div>
+        <NavBar src={writefab} path="/post/create" />
+      </Container>
+    );
+  }
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/feed/MyFeedPage.tsx` around lines 75 - 77, The current MyFeedPage
returns an empty fragment on error (if (error) return <></>), leaving users
stranded; replace that with the same error UI + back navigation used in
GroupDetail/ParticipatedGroupDetail: render a user-friendly error message and a
visible "Back" control that triggers the page navigation (e.g., the same
BackButton or useNavigate/navigation callback used elsewhere) and preserve any
error details for logging or debugging; update the error branch inside the
MyFeedPage component to mirror the error-handling pattern from
GroupDetail/ParticipatedGroupDetail instead of returning an empty fragment.
src/pages/feed/FollowerListPage.tsx (1)

61-70: โš ๏ธ Potential issue | ๐ŸŸ  Major

response null ์ฒดํฌ ์ˆœ์„œ๊ฐ€ ์ž˜๋ชป๋˜์—ˆ์Šต๋‹ˆ๋‹ค โ€” ์ด๋ฏธ ๋ฐ์ดํ„ฐ๋ฅผ ์ ‘๊ทผํ•œ ์ดํ›„์— ์ฒดํฌํ•ฉ๋‹ˆ๋‹ค.

response.data๊ฐ€ lines 62, 64์—์„œ ๋จผ์ € ์ ‘๊ทผ๋œ ๋’ค lines 67-70์—์„œ null ์ฒดํฌ๊ฐ€ ์ˆ˜ํ–‰๋ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ response๋‚˜ response.data๊ฐ€ falsy๋ผ๋ฉด line 62์—์„œ ์ด๋ฏธ ๋Ÿฐํƒ€์ž„ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. null ์ฒดํฌ๋ฅผ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ์ด์ „์œผ๋กœ ์ด๋™ํ•˜๊ฑฐ๋‚˜, ๋ถˆํ•„์š”ํ•˜๋‹ค๋ฉด ์ œ๊ฑฐํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ› ์ˆ˜์ • ์ œ์•ˆ
+        if (!response || !response.data) {
+          setError('API ์‘๋‹ต์ด ์—†์Šต๋‹ˆ๋‹ค.');
+          return;
+        }
+
         let userData: FollowData[] = [];
         if (type === 'followerlist') {
           userData = (response.data as { followers: FollowData[] })?.followers || [];
         } else {
           userData = (response.data as { followings: FollowData[] })?.followings || [];
         }

-        if (!response || !response.data) {
-          setError('API ์‘๋‹ต์ด ์—†์Šต๋‹ˆ๋‹ค.');
-          return;
-        }
-
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/feed/FollowerListPage.tsx` around lines 61 - 70, Move the
null-check for response/response.data before any access to response.data to
avoid runtime errors: check if response and response.data are truthy (e.g., if
(!response || !response.data) { setError('API ์‘๋‹ต์ด ์—†์Šต๋‹ˆ๋‹ค.'); return; }) before
computing userData, then populate userData using the existing type switch that
references (response.data as { followers: FollowData[] }) or followings;
alternatively replace direct access with safe optional chaining when assigning
userData (e.g., response?.data?.followers) but ensure setError/return still runs
when data is missing; update the logic around the variables response, userData,
type and setError in FollowerListPage.tsx accordingly.
src/pages/groupSearch/GroupSearch.tsx (1)

50-59: โš ๏ธ Potential issue | ๐ŸŸ  Major

์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด๋ฅผ ์ดˆ๊ธฐ ๋ Œ๋”์—์„œ ์ค‘๋ณต ํ˜ธ์ถœํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

Line 50-53๊ณผ Line 56-58์ด ๋งˆ์šดํŠธ ์‹œ์ ์— ๋™์‹œ์— ์‹คํ–‰๋˜์–ด fetchRecentSearches()๊ฐ€ 2ํšŒ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค(searchStatus ์ดˆ๊ธฐ๊ฐ’์ด idle). ์ดˆ๊ธฐ ์š”์ฒญ ์ค‘๋ณต๊ณผ ๋กœ๋”ฉ ์ƒํƒœ ๊นœ๋นก์ž„์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”ง ์ œ์•ˆ ์ˆ˜์ •์•ˆ
-  useEffect(() => {
-    fetchRecentSearches();
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, []);
-
   useEffect(() => {
     if (searchStatus === 'idle') {
       fetchRecentSearches();
     }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [searchStatus]);
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/groupSearch/GroupSearch.tsx` around lines 50 - 59, The useEffect
hooks call fetchRecentSearches() twice on mount because searchStatus starts as
'idle'โ€”fix by consolidating the logic: keep a single useEffect (the one with
empty deps or the one watching searchStatus) and add a guard so
fetchRecentSearches() is invoked only when searchStatus === 'idle' (or remove
the effect that watches searchStatus entirely and rely on the mount effect with
the status check); update references to useEffect, fetchRecentSearches, and
searchStatus accordingly to avoid duplicate initial requests and loading
flicker.
src/pages/groupDetail/GroupDetail.tsx (1)

55-57: โš ๏ธ Potential issue | ๐ŸŸก Minor

isLoading ์ดˆ๊ธฐ๊ฐ’์ด false๋กœ ์„ค์ •๋˜์–ด ParticipatedGroupDetail๊ณผ ๋ถˆ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค.

ParticipatedGroupDetail.tsx์—์„œ๋Š” isLoading์ด true๋กœ ์ดˆ๊ธฐํ™”๋˜์ง€๋งŒ, ์—ฌ๊ธฐ์„œ๋Š” false์ž…๋‹ˆ๋‹ค. !roomData ๊ฐ€๋“œ ๋•๋ถ„์— ์Šค์ผˆ๋ ˆํ†ค์ด ํ‘œ์‹œ๋˜๊ธด ํ•˜์ง€๋งŒ, setIsLoading(true)๊ฐ€ ์‹คํ–‰๋˜๊ธฐ ์ „ ์ฒซ ๋ Œ๋”๋ง์—์„œ isLoading์ด false์ธ ์ƒํƒœ๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. ์ผ๊ด€์„ฑ์„ ์œ„ํ•ด true๋กœ ์ดˆ๊ธฐํ™”ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”ง ์ œ์•ˆ
-  const [isLoading, setIsLoading] = useState(false);
+  const [isLoading, setIsLoading] = useState(true);
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/groupDetail/GroupDetail.tsx` around lines 55 - 57, The isLoading
state in GroupDetail (useState for isLoading) is initialized to false which
disagrees with ParticipatedGroupDetail; change the initial value to true so the
first render reflects loading semantics (update the useState<boolean> call that
declares isLoading and setIsLoading in GroupDetail.tsx to use true) to keep
behavior consistent with ParticipatedGroupDetail and avoid a non-loading initial
render before setIsLoading(true) runs.
src/pages/groupDetail/ParticipatedGroupDetail.tsx (1)

56-63: โš ๏ธ Potential issue | ๐ŸŸก Minor

isLoading์˜ ์ดˆ๊ธฐ๊ฐ’์ด true๋กœ ์„ค์ •๋˜์–ด ์žˆ์–ด, ์ฒซ ๋ Œ๋”๋ง ์‹œ ์Šค์ผˆ๋ ˆํ†ค์ด ๋ฐ”๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.

์ด ๋ถ€๋ถ„์€ ์˜ฌ๋ฐ”๋ฅธ ์ ‘๊ทผ์ž…๋‹ˆ๋‹ค. GroupDetail.tsx์—์„œ๋Š” isLoading์˜ ์ดˆ๊ธฐ๊ฐ’์ด false๋กœ ์„ค์ •๋˜์–ด ์žˆ์–ด (Line 56) !roomData ๊ฐ€๋“œ์—๋งŒ ์˜์กดํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๋‘ ํŽ˜์ด์ง€ ๊ฐ„ ์ดˆ๊ธฐ๊ฐ’์ด ๋‹ค๋ฅธ ์ ์ด ์˜๋„๋œ ๊ฒƒ์ธ์ง€ ํ™•์ธํ•ด ์ฃผ์„ธ์š” โ€” GroupDetail.tsx์—์„œ๋Š” ์ฒซ ๋ Œ๋”๋ง ์‹œ ์Šค์ผˆ๋ ˆํ†ค ๋Œ€์‹  ๋นˆ ํ™”๋ฉด์ด ์ˆœ๊ฐ„์ ์œผ๋กœ ๋‚˜ํƒ€๋‚  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/groupDetail/ParticipatedGroupDetail.tsx` around lines 56 - 63, The
ParticipatedGroupDetail component initializes loading as true which immediately
shows the skeleton on first render; make the initial loading value consistent
with GroupDetail.tsx by setting the loading state (const [loading, setLoading])
to false (or align both components' intent) and rely on the !roomData guard for
initial empty-render behavior, updating setLoading(true/false) only around your
async fetch logic (functions referenced: ParticipatedGroupDetail, setLoading,
roomData) so both pages behave consistently.
โ™ป๏ธ Duplicate comments (3)
src/components/search/MostSearchedBooks.tsx (1)

30-34: ์ตœ์†Œ ๋กœ๋”ฉ ๋ณด์žฅ ๋กœ์ง ์ด์Šˆ๊ฐ€ ๋ฐ˜๋ณต๋ฉ๋‹ˆ๋‹ค.

์ด ๊ตฌ๊ฐ„๋„ API ์‹คํŒจ ์‹œ Promise.all์ด ์ฆ‰์‹œ reject๋˜์–ด ์Šค์ผˆ๋ ˆํ†ค ์ตœ์†Œ ๋…ธ์ถœ ์‹œ๊ฐ„์ด ๋ณด์žฅ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/search/MostSearchedBooks.tsx` around lines 30 - 34, The
Promise.all usage with getMostSearchedBooks and minLoadingTime can reject
immediately when the API fails, breaking the guaranteed skeleton display; change
the logic to await both promises regardless of API failure (e.g., use
Promise.allSettled([getMostSearchedBooks(), minLoadingTime]) and then extract
the fulfilled value from the first result or fall back to safe empty data), or
wrap getMostSearchedBooks() with a catch that returns a sentinel/fallback so
Promise.all always resolves; update references in this component
(minLoadingTime, getMostSearchedBooks) accordingly.
src/pages/feed/OtherFeedPage.tsx (1)

37-46: ์ตœ์†Œ ๋กœ๋”ฉ ๋ณด์žฅ ๋กœ์ง ์ด์Šˆ๊ฐ€ ๋™์ผํ•ฉ๋‹ˆ๋‹ค.

์—ฌ๊ธฐ๋„ Promise.all fail-fast ํŠน์„ฑ ๋•Œ๋ฌธ์— ์‹คํŒจ ์‹œ 500ms ๋ณด์žฅ์ด ๊นจ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋™์ผ ํŒจํ„ด์œผ๋กœ ๋ณด์ •ํ•ด ์ฃผ์„ธ์š”.

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/feed/OtherFeedPage.tsx` around lines 37 - 46, The current
Promise.all with getOtherFeed, getOtherProfile and minLoadingTime can fail-fast
and skip the 500ms guarantee; wrap the API calls so the timing promise always
resolves independentlyโ€”e.g., use Promise.allSettled or attach .catch handlers to
getOtherFeed/getOtherProfile before awaitingโ€”then after awaiting, check results
and throw or handle errors and extract values into feedResponse/profileResponse;
update the block referencing getOtherFeed, getOtherProfile, minLoadingTime,
feedResponse and profileResponse accordingly.
src/pages/today-words/TodayWords.tsx (1)

94-101: ์ตœ์†Œ ๋กœ๋”ฉ ๋ณด์žฅ ๋กœ์ง ์ด์Šˆ๊ฐ€ ๋™์ผํ•˜๊ฒŒ ๋ฐ˜๋ณต๋ฉ๋‹ˆ๋‹ค.

Promise.all ๊ธฐ๋ฐ˜์ด๋ผ API ์‹คํŒจ ์‹œ ์ตœ์†Œ 500ms ๋…ธ์ถœ์ด ๋ณด์žฅ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์œ„์—์„œ ์ง€์ ํ•œ ํŒจํ„ด๊ณผ ๋™์ผํ•˜๊ฒŒ ์ •๋ฆฌํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/today-words/TodayWords.tsx` around lines 94 - 101, The current
Promise.all usage with minLoadingTime plus getDailyGreeting means if
getDailyGreeting rejects the 500ms delay may be skipped; change to ensure
minLoadingTime is always awaited by either awaiting minLoadingTime in a finally
block or by converting the API call into a non-rejecting promise and then using
Promise.all/Promise.allSettled. Concretely, wrap getDailyGreeting(roomId:
parseInt(roomId), cursor: cursor || undefined) into a promise that catches
errors (so it never rejects) or use Promise.allSettled for
[getDailyGreeting(...), minLoadingTime] and then handle the settled result for
the API call (check for rejection and process accordingly); update references in
TodayWords to use the new safe API result handling and keep isRefresh logic the
same.
๐Ÿงน Nitpick comments (16)
src/shared/ui/Skeleton/base/Skeleton.styled.ts (2)

54-56: SkeletonTextLine์—์„œ height ๋ณด๊ฐ„ ์ค‘๋ณต ์„ ์–ธ

SkeletonBase์—์„œ ์ด๋ฏธ height prop ๋ณด๊ฐ„ ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. SkeletonTextLine์ด ์ด๋ฅผ ์žฌ์„ ์–ธํ•˜๋Š” ์œ ์ผํ•œ ์ด์œ ๋Š” ๊ธฐ๋ณธ๊ฐ’์„ 20px์—์„œ 16px๋กœ ๋ณ€๊ฒฝํ•˜๊ธฐ ์œ„ํ•จ์ž…๋‹ˆ๋‹ค. ์ด ํŒจํ„ด์€ ํ˜ผ๋ž€์Šค๋Ÿฌ์šธ ์ˆ˜ ์žˆ์œผ๋ฉฐ, ํ–ฅํ›„ SkeletonBase์˜ height ๋กœ์ง์ด ๋ณ€๊ฒฝ๋  ๊ฒฝ์šฐ ๋‘ ๊ณณ์„ ๋ชจ๋‘ ์ˆ˜์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

โ™ป๏ธ ๊ธฐ๋ณธ๊ฐ’๋งŒ ๋ช…์‹œ์ ์œผ๋กœ ๋ถ„๋ฆฌํ•˜๋Š” ๋ฐฉ์‹ ์ œ์•ˆ
-export const SkeletonTextLine = styled(SkeletonBase)`
-  height: ${({ height }) => (typeof height === 'number' ? `${height}px` : height || '16px')};
-  border-radius: 4px;
-`;
+export const SkeletonTextLine = styled(SkeletonBase).attrs<SkeletonBaseProps>(({ height }) => ({
+  height: height ?? 16,
+}))`
+  border-radius: 4px;
+`;
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/Skeleton/base/Skeleton.styled.ts` around lines 54 - 56,
SkeletonTextLine duplicates the height interpolation already handled by
SkeletonBase; remove the redundant height handling from SkeletonTextLine and
instead rely on SkeletonBase's logic while only overriding the default value
(from 20px to 16px) via a single source of truthโ€”e.g., pass a default prop or
theme/variable used by SkeletonBaseโ€”so update SkeletonTextLine to stop
re-declaring height and supply the desired default height through SkeletonBase's
prop/default mechanism (reference SkeletonTextLine and SkeletonBase and the
height prop).

22-36: display: inline-block์œผ๋กœ ์ธํ•œ ๋น„(้ž) flex ์ปจํ…Œ์ด๋„ˆ์—์„œ์˜ ์˜ˆ๊ธฐ์น˜ ์•Š์€ ๋ ˆ์ด์•„์›ƒ ๊ฐ€๋Šฅ์„ฑ

SkeletonBase๊ฐ€ flex/grid๊ฐ€ ์•„๋‹Œ ์ผ๋ฐ˜ ๋ธ”๋ก ์ปจํ…Œ์ด๋„ˆ์˜ ์ž์‹์œผ๋กœ ๋ฐฐ์น˜๋  ๊ฒฝ์šฐ, inline-block ํŠน์„ฑ ๋•Œ๋ฌธ์— ์—ฌ๋Ÿฌ ์Šค์ผˆ๋ ˆํ†ค ์š”์†Œ๊ฐ€ ์„ธ๋กœ๋กœ ์Œ“์ด์ง€ ์•Š๊ณ  ๊ฐ€๋กœ๋กœ ๋‚˜๋ž€ํžˆ ํ๋ฅผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. flex ์ปจํ…์ŠคํŠธ ๋‚ด์—์„œ๋Š” ์™ธ๋ถ€ ๋””์Šคํ”Œ๋ ˆ์ด ์œ ํ˜•์ด ์žฌ์ •์˜๋˜๋ฏ€๋กœ ๋ฌธ์ œ๊ฐ€ ์—†์ง€๋งŒ, ์ฝ”๋“œ๋ฒ ์ด์Šค ์ „๋ฐ˜์— ๊ฑธ์ณ ๋น„ flex ์ปจํ…์ŠคํŠธ์—์„œ๋„ ์‚ฌ์šฉ ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ๋‹ค๋ฉด display: block์ด ๋” ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ๊ธฐ๋ณธ๊ฐ’์ž…๋‹ˆ๋‹ค.

โ™ป๏ธ ์ œ์•ˆ: block์œผ๋กœ ๋ณ€๊ฒฝ
 export const SkeletonBase = styled.div<SkeletonBaseProps>`
-  display: inline-block;
+  display: block;
   width: ${({ width }) => (typeof width === 'number' ? `${width}px` : width || '100%')};
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/Skeleton/base/Skeleton.styled.ts` around lines 22 - 36, The
SkeletonBase styled component currently uses display: inline-block which can
cause multiple skeletons to flow horizontally in non-flex/grid parents; update
SkeletonBase to use display: block so it behaves as a predictable full-width
block element in plain containers, keeping the existing
width/height/borderRadius logic and animations intact (locate SkeletonBase in
src/shared/ui/Skeleton/base/Skeleton.styled.ts and replace the display rule from
inline-block to block).
src/shared/ui/Skeleton/search/RecentSearchTabsSkeleton.tsx (1)

4-15: ์Šค์ผˆ๋ ˆํ†ค ๋‚ด ํ•˜๋“œ์ฝ”๋”ฉ๋œ ํƒ€์ดํ‹€ ํ…์ŠคํŠธ โ€” ์‹ค์ œ ์ปดํฌ๋„ŒํŠธ์™€ ํ…์ŠคํŠธ ๋™๊ธฐํ™” ํ•„์š”

Title์— "์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด"๊ฐ€ ๋ฆฌํ„ฐ๋Ÿด๋กœ ํ•˜๋“œ์ฝ”๋”ฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์‹ค์ œ RecentSearchTabs ์ปดํฌ๋„ŒํŠธ์˜ ํƒ€์ดํ‹€ ํ…์ŠคํŠธ๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ์ด ์Šค์ผˆ๋ ˆํ†ค๋„ ํ•จ๊ป˜ ์ˆ˜๋™์œผ๋กœ ์—…๋ฐ์ดํŠธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํƒ€์ดํ‹€๋„ ์Šค์ผˆ๋ ˆํ†ค์œผ๋กœ ๋Œ€์ฒดํ•˜๊ฑฐ๋‚˜, ์‹ค์ œ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ƒ์ˆ˜๋ฅผ ๊ณต์œ ํ•˜๋Š” ๋ฐฉ์‹์„ ๊ณ ๋ คํ•˜์„ธ์š”.

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/Skeleton/search/RecentSearchTabsSkeleton.tsx` around lines 4 -
15, RecentSearchTabsSkeleton currently hardcodes the Title text ("์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด"),
causing drift if RecentSearchTabs' title changes; update
RecentSearchTabsSkeleton to avoid literal text by either rendering the same
shared constant used by RecentSearchTabs (import the title constant) or
rendering a Skeleton placeholder for the title (replace Title's literal children
with a Skeleton.Box/Skeleton.Text), targeting the RecentSearchTabsSkeleton
component and the Title element so the skeleton stays in sync with the real
component.
src/shared/ui/Skeleton/searchBook/BookItemSkeleton.tsx (1)

18-23: ์ŠคํŽ˜์ด์‹ฑ์„ ์œ„ํ•œ ์ธ๋ผ์ธ ์Šคํƒ€์ผ div ์‚ฌ์šฉ โ€” styled-component ํŒจํ„ด๊ณผ ๋ถˆ์ผ์น˜

์„ธ ๋ฒˆ์งธ ์Šค์ผˆ๋ ˆํ†ค ํ…์ŠคํŠธ ๋ผ์ธ์˜ ์ƒ๋‹จ ์—ฌ๋ฐฑ์„ ์œ„ํ•ด bare <div style={{ marginTop: '8px' }}> ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋™์ผ ์ปดํฌ๋„ŒํŠธ ๋‚ด ๋‹ค๋ฅธ ๋ ˆ์ด์•„์›ƒ์ด styled-component๋ฅผ ํ†ตํ•ด ์ฒ˜๋ฆฌ๋˜๋Š” ๊ฒƒ๊ณผ ์ผ๊ด€์„ฑ์ด ์—†์Šต๋‹ˆ๋‹ค. Skeleton.Text์— margin ๋˜๋Š” style prop์„ ์ง์ ‘ ์ „๋‹ฌํ•˜๊ฑฐ๋‚˜, BookInfo ๋‚ด๋ถ€ ์Šคํƒ€์ผ๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

โ™ป๏ธ ์ œ์•ˆ
-        <div style={{ marginTop: '8px' }}>
-          <Skeleton.Text width={150} height={12} />
-        </div>
+        <Skeleton.Text width={150} height={12} style={{ marginTop: '8px' }} />

๋‹จ, Skeleton.Text๊ฐ€ style prop์„ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ, SkeletonProps์— style?: React.CSSProperties๋ฅผ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ๋ž˜ํผ styled-component๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/Skeleton/searchBook/BookItemSkeleton.tsx` around lines 18 - 23,
ํ˜„์žฌ BookItemSkeleton.tsx์—์„œ BookInfo ๋‚ด๋ถ€์— bare <div style={{ marginTop: '8px' }}>๋กœ
์ธ๋ผ์ธ ์Šคํƒ€์ผ์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์–ด styled-component ํŒจํ„ด๊ณผ ๋ถˆ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค; BookInfo ์•ˆ์˜ ์„ธ ๋ฒˆ์งธ Skeleton.Text ์ƒ๋‹จ
์—ฌ๋ฐฑ์„ ์œ„ํ•ด inline div๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  ๋Œ€์‹  Skeleton.Text์— margin/top ์Šคํƒ€์ผ์„ ์ „๋‹ฌํ•˜๊ฑฐ๋‚˜(๋งŒ์•ฝ
Skeleton.Text๊ฐ€ style prop์„ ์ง€์›ํ•˜์ง€ ์•Š์œผ๋ฉด SkeletonProps์— style?: React.CSSProperties๋ฅผ
์ถ”๊ฐ€) ๋˜๋Š” BookInfo ๋‚ด๋ถ€์— ๋ž˜ํผ styled-component(์˜ˆ: InfoGap ๋˜๋Š” StyledSkeletonWrap)๋ฅผ ๋งŒ๋“ค์–ด
margin-top์„ ์ ์šฉํ•˜๋„๋ก ๋ณ€๊ฒฝํ•˜์„ธ์š”; ์ฐธ์กฐํ•  ์‹๋ณ„์ž๋Š” BookItemSkeleton.tsx, BookInfo,
Skeleton.Text์ž…๋‹ˆ๋‹ค.
src/shared/ui/Skeleton/base/Skeleton.tsx (1)

40-44: Skeleton.Box์™€ ๊ธฐ๋ณธ Skeleton์˜ ์ค‘๋ณต

Skeleton(base)๊ณผ Skeleton.Box๋Š” ์„œ๋กœ ๋‹ค๋ฅธ styled ์ปดํฌ๋„ŒํŠธ(SkeletonBase vs SkeletonBox)๋ฅผ ์‚ฌ์šฉํ•˜์ง€๋งŒ, SkeletonBox๋Š” SkeletonBase์˜ ๋นˆ ํ™•์žฅ(styled(SkeletonBase)```)์œผ๋กœ ์ •์˜๋˜์–ด ์žˆ์–ด ์‹ค์งˆ์ ์œผ๋กœ ๋™์ผํ•ฉ๋‹ˆ๋‹ค. ๋‘ API ํ‘œ๋ฉด ๊ฐ„์˜ ์˜๋ฏธ์  ๊ตฌ๋ถ„์ด ์—†์œผ๋ฏ€๋กœ, ์™ธ๋ถ€ ์†Œ๋น„์ž๊ฐ€ ๋‘˜ ์ค‘ ์–ด๋А ๊ฒƒ์„ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š”์ง€ ํ˜ผ๋ž€์Šค๋Ÿฌ์šธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Skeleton.Box๋ฅผ ์œ ์ผํ•œ ๋ช…์‹œ์  ์ง์‚ฌ๊ฐํ˜• ์Šค์ผˆ๋ ˆํ†ค API๋กœ ์‚ฌ์šฉํ•˜๊ณ  ๊ธฐ๋ณธ Skeleton` export๋Š” ๋‚ด๋ถ€ ์ „์šฉ์œผ๋กœ ์ œํ•œํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•˜์„ธ์š”.

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/Skeleton/base/Skeleton.tsx` around lines 40 - 44, The base and
box APIs are redundant: SkeletonBox is just a styled(SkeletonBase) empty
extension and Skeleton.Box duplicates functionality; make the rectangular API
explicit by keeping Skeleton.Box (Skeleton.Box component) as the public export
and restrict the base Skeleton (Skeleton and SkeletonBase) to internal use
onlyโ€”remove or stop exporting the top-level Skeleton component, keep
SkeletonBox/Skeleton.Box as the single public rectangle API, update any export
lists and consume-sites to import Skeleton.Box instead of Skeleton, and add a
deprecation note/alias if needed to preserve backwards compatibility.
src/shared/ui/Skeleton/memory/RecordItemSkeleton.tsx (1)

12-36: ์ธ๋ผ์ธ ์Šคํƒ€์ผ ์ผ๋ถ€๋Š” styled ์ปดํฌ๋„ŒํŠธ๋กœ ๋ถ„๋ฆฌํ•˜๋ฉด ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ๋” ์‰ฌ์›Œ์ง‘๋‹ˆ๋‹ค.

ํ˜„์žฌ ๋ฐ˜๋ณต๋˜๋Š” ์ธ๋ผ์ธ ์Šคํƒ€์ผ(div 3๊ณณ)์€ ์ถ”ํ›„ ์Šค์ผˆ๋ ˆํ†ค ๋””์ž์ธ ๋ณ€๊ฒฝ ์‹œ ์ˆ˜์ • ํฌ์ธํŠธ๋ฅผ ๋Š˜๋ฆฝ๋‹ˆ๋‹ค. ๊ฒฝ๋Ÿ‰ ๋ž˜ํผ๋กœ ์ถ”์ถœํ•ด ๋‘๋ฉด ์ผ๊ด€์„ฑ ๊ด€๋ฆฌ๊ฐ€ ๋” ํŽธํ•ด์ง‘๋‹ˆ๋‹ค.

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/Skeleton/memory/RecordItemSkeleton.tsx` around lines 12 - 36,
The three repeated inline divs inside the RecordItemSkeleton (the ones wrapping
Skeleton.Box + Skeleton.Text in ActionSection and the circle wrapper in
UserSection) should be replaced with a lightweight styled component to remove
duplicated inline styles; create a styled component (e.g., IconRow or InlineRow)
and use it in place of the divs in Container/UserSection/ActionSection so styles
like display:flex, gap, alignItems and marginRight are centralized and applied
consistently across the component.
src/shared/ui/Skeleton/group/HotTopicSectionSkeleton.tsx (1)

15-22: ์ธ๋ผ์ธ ์Šคํƒ€์ผ์„ styled-component๋กœ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๋‚ด๋ถ€ ๋ž˜ํผ div๋“ค์— ์ธ๋ผ์ธ ์Šคํƒ€์ผ์ด ์‚ฌ์šฉ๋˜๊ณ  ์žˆ์œผ๋‚˜, ํ•ด๋‹น ํŒŒ์ผ์˜ ๋‚˜๋จธ์ง€ ๋ถ€๋ถ„์€ GroupDetailSkeleton.styled ๊ธฐ๋ฐ˜์˜ styled-component๋ฅผ ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ธ๋ผ์ธ ์Šคํƒ€์ผ ๊ฐ์ฒด๋Š” ๋งค ๋ Œ๋”๋งˆ๋‹ค ์ƒˆ๋กœ ์ƒ์„ฑ๋˜๊ณ  ์ฝ”๋“œ ์ผ๊ด€์„ฑ๋„ ๋‚ฎ์•„์ง‘๋‹ˆ๋‹ค.

โ™ป๏ธ ๋ฆฌํŒฉํ„ฐ๋ง ์ œ์•ˆ

HotTopicSectionSkeleton.styled.ts (๋˜๋Š” GroupDetailSkeleton.styled.ts)์— ์•„๋ž˜ ์Šคํƒ€์ผ์„ ์ถ”๊ฐ€ํ•˜๊ณ  ์ธ๋ผ์ธ ์Šคํƒ€์ผ์„ ๊ต์ฒดํ•ฉ๋‹ˆ๋‹ค:

+export const HotTopicContentPadding = styled.div`
+  padding: 16px 0;
+`;
+
+export const HotTopicContentTitle = styled.div`
+  margin-bottom: 16px;
+`;
+
+export const HotTopicBoxList = styled.div`
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+`;
-        <div style={{ padding: '16px 0' }}>
-          <div style={{ marginBottom: '16px' }}>
-            <Skeleton.Text width="80%" height={16} />
-          </div>
-          <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
-            <Skeleton.Box width="100%" height={40} borderRadius={8} />
-            <Skeleton.Box width="100%" height={40} borderRadius={8} />
-          </div>
-        </div>
+        <HotTopicContentPadding>
+          <HotTopicContentTitle>
+            <Skeleton.Text width="80%" height={16} />
+          </HotTopicContentTitle>
+          <HotTopicBoxList>
+            <Skeleton.Box width="100%" height={40} borderRadius={8} />
+            <Skeleton.Box width="100%" height={40} borderRadius={8} />
+          </HotTopicBoxList>
+        </HotTopicContentPadding>
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/Skeleton/group/HotTopicSectionSkeleton.tsx` around lines 15 -
22, HotTopicSectionSkeleton currently uses inline style objects on the outer
wrapper and two inner divs; extract those into styled-components (e.g., create
StyledHotTopicWrapper, StyledTitleWrapper, StyledListWrapper in
HotTopicSectionSkeleton.styled.ts or reuse GroupDetailSkeleton.styled) and
replace the inline divs in the HotTopicSectionSkeleton component with these
styled components, preserving the same CSS (padding: 16px 0; margin-bottom:
16px; and display:flex; flex-direction:column; gap:8px;) and keep existing
Skeleton children and props unchanged.
src/shared/ui/Skeleton/members/MemberListSkeleton.tsx (1)

16-18: Skeleton.Circle ์ฃผ๋ณ€์˜ ๋ถˆํ•„์š”ํ•œ ๋ž˜ํผ <div> ์ œ๊ฑฐ๋ฅผ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

ProfileSection์€ ์ด๋ฏธ display: flex; align-items: center; gap: 8px ๋ ˆ์ด์•„์›ƒ์„ ๊ฐ–์ถ”๊ณ  ์žˆ์–ด Skeleton.Circle์„ ์ง์ ‘ ๋ฐฐ์น˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋นˆ <div>๋Š” ๋ถˆํ•„์š”ํ•œ DOM ๋…ธ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

โ™ป๏ธ ๋ฆฌํŒฉํ„ฐ๋ง ์ œ์•ˆ
           <ProfileSection>
-            <div>
-              <Skeleton.Circle width={36} />
-            </div>
+            <Skeleton.Circle width={36} />
             <MemberInfo>
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/Skeleton/members/MemberListSkeleton.tsx` around lines 16 - 18,
Remove the unnecessary wrapper <div> around Skeleton.Circle in the
MemberListSkeleton component: because ProfileSection already applies
display:flex, align-items:center and gap:8px, place Skeleton.Circle directly as
a child (remove the extra div node) so the DOM is simpler and layout remains
unchanged; update the JSX in MemberListSkeleton (where Skeleton.Circle is
rendered) to render Skeleton.Circle inline and verify styling via ProfileSection
still centers and gaps correctly.
src/shared/ui/Skeleton/group/BannerSkeleton.tsx (1)

15-20: marginTop ์ธ๋ผ์ธ ์Šคํƒ€์ผ์„ styled-component๋กœ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

HotTopicSectionSkeleton๊ณผ ๋™์ผํ•˜๊ฒŒ, ๋‚ด๋ถ€ ๋ž˜ํผ div์— ์ธ๋ผ์ธ ์Šคํƒ€์ผ์ด ์‚ฌ์šฉ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. GroupDetailSkeleton.styled.ts์— ๊ด€๋ จ ์Šคํƒ€์ผ์ด ์ด๋ฏธ ์กด์žฌํ•˜๋ฏ€๋กœ(MetaSkeletonRow, TagSkeletonRow ๋“ฑ) ์ผ๊ด€์„ฑ์„ ์œ„ํ•ด ์Šคํƒ€์ผ์„ styled-component๋กœ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/Skeleton/group/BannerSkeleton.tsx` around lines 15 - 20, The
inline marginTop in BannerSkeleton.tsx should be moved to a styled-component in
GroupDetailSkeleton.styled.ts for consistency; create a new styled export (e.g.,
BannerSkeletonWrapper or reuse an existing pattern like
MetaSkeletonRow/TagSkeletonRow), apply the margin-top: 20px (and the inner 8px
spacer as another styled div or apply a margin-top on a new InnerWrapper),
export and import that styled into BannerSkeleton.tsx, then replace the two
inline-styled divs with the new styled components so no inline styles remain.
src/pages/feed/MyFeedPage.tsx (1)

38-46: Promise.all ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ ์ˆ˜๋™ ํƒ€์ž… ์บ์ŠคํŠธ๊ฐ€ ์ทจ์•ฝํ•ฉ๋‹ˆ๋‹ค.

Promise.all์˜ ์„ธ ๋ฒˆ์งธ ์ธ์ž(minLoadingTime)๊ฐ€ ํƒ€์ž… ์ถ”๋ก ์„ ๋ฐฉํ•ดํ•˜์—ฌ ์ „์ฒด ๊ฒฐ๊ณผ๋ฅผ ์ˆ˜๋™์œผ๋กœ ์บ์ŠคํŒ…ํ•ด์•ผ ํ•˜๋Š” ๊ตฌ์กฐ๊ฐ€ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋ฐฐ์—ด ์ˆœ์„œ๊ฐ€ ๋ณ€๊ฒฝ๋  ๊ฒฝ์šฐ ์บ์ŠคํŠธ๊ฐ€ ๋Ÿฐํƒ€์ž„ ๋ฒ„๊ทธ๋ฅผ ์กฐ์šฉํžˆ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.

โ™ป๏ธ ๋ฆฌํŒฉํ„ฐ๋ง ์ œ์•ˆ
-        const [feedResponse, profileResponse] = (await Promise.all([
-          getOtherFeed(Number(userId)),
-          getOtherProfile(Number(userId)),
-          minLoadingTime,
-        ])) as [
-          Awaited<ReturnType<typeof getOtherFeed>>,
-          Awaited<ReturnType<typeof getOtherProfile>>,
-          void,
-        ];
+        const [feedResponse, profileResponse] = await Promise.all([
+          getOtherFeed(Number(userId)),
+          getOtherProfile(Number(userId)),
+        ] as const);
+        await minLoadingTime;
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/feed/MyFeedPage.tsx` around lines 38 - 46, The current Promise.all
includes minLoadingTime in the same array which forces a manual cast and risks
silent runtime bugs if array order changes; change the code to await the two
network calls together and await minLoadingTime separately: call
Promise.all([getOtherFeed(Number(userId)), getOtherProfile(Number(userId))]) to
get [feedResponse, profileResponse] (no manual cast), then await minLoadingTime
on its own (e.g., await minLoadingTime) before continuing; update references to
feedResponse and profileResponse accordingly.
src/pages/mypage/Mypage.tsx (1)

109-118: ์ค‘๋ณต๋œ ํ”„๋กœํ•„ ์Šค์ผˆ๋ ˆํ†ค ๋งˆํฌ์—…์€ ๊ณต์šฉ ์ปดํฌ๋„ŒํŠธ ์žฌ์‚ฌ์šฉ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

ํ˜„์žฌ ๊ตฌ์กฐ๋Š” src/shared/ui/Skeleton/feed/ProfileSkeleton.tsx์™€ ์—ญํ• ์ด ๊ฒน์ณ ์œ ์ง€๋ณด์ˆ˜ ํฌ์ธํŠธ๊ฐ€ ๋Š˜์–ด๋‚ฉ๋‹ˆ๋‹ค. ๊ณต์šฉ ์Šค์ผˆ๋ ˆํ†ค์œผ๋กœ ์น˜ํ™˜ํ•˜๋ฉด ์Šคํƒ€์ผ ๋ณ€๊ฒฝ ์‹œ ์ผ๊ด€์„ฑ์ด ์ข‹์•„์ง‘๋‹ˆ๋‹ค.

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/mypage/Mypage.tsx` around lines 109 - 118, The markup duplicated in
Mypage.tsx (ProfileSkeletonContainer / ProfileSkeletonLeft / ProfileSkeletonText
with Skeleton.Circle/Text/Box) should be replaced by the shared ProfileSkeleton
component from src/shared/ui/Skeleton/feed/ProfileSkeleton.tsx; locate the block
that renders ProfileSkeletonContainer and swap it for the shared
<ProfileSkeleton /> (or pass the existing size/variant props if ProfileSkeleton
supports them) so styling and behavior come from the single source of truth and
the local skeleton subcomponents (ProfileSkeletonContainer, ProfileSkeletonLeft,
ProfileSkeletonText, Skeleton.Circle, Skeleton.Text, Skeleton.Box) are removed
from this file.
src/pages/groupDetail/ParticipatedGroupDetail.tsx (3)

80-86: minLoadingTime์˜ resolve ํƒ€์ž…์ด ๋ช…์‹œ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.

new Promise(resolve => setTimeout(resolve, 500))์—์„œ resolve์˜ ํƒ€์ž…์ด ์ถ”๋ก ์œผ๋กœ Promise<unknown>์ด ๋ฉ๋‹ˆ๋‹ค. ๊ธฐ๋Šฅ์ƒ ๋ฌธ์ œ๋Š” ์—†์ง€๋งŒ, ์—ฌ๋Ÿฌ ๊ณณ์—์„œ ๋ฐ˜๋ณต๋˜๋Š” ํŒจํ„ด์ด๋ฏ€๋กœ ์œ ํ‹ธ ํ•จ์ˆ˜๋กœ ์ถ”์ถœํ•˜๋ฉด ์ฝ”๋“œ ์ค‘๋ณต์„ ์ค„์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

โ™ป๏ธ ์œ ํ‹ธ ํ•จ์ˆ˜ ์ถ”์ถœ ์ œ์•ˆ
// e.g. src/utils/delay.ts
export const delay = (ms: number): Promise<void> =>
  new Promise(resolve => setTimeout(resolve, ms));

๊ทธ ํ›„ ๊ฐ ํŒŒ์ผ์—์„œ:

-const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500));
+const minLoadingTime = delay(500);
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/groupDetail/ParticipatedGroupDetail.tsx` around lines 80 - 86,
Replace the inline new Promise pattern used as minLoadingTime with a reusable
typed utility (e.g., export const delay = (ms: number): Promise<void>) and use
it in the ParticipatedGroupDetail component when awaiting getRoomPlaying;
specifically, remove new Promise(resolve => setTimeout(resolve, 500)) and call
await delay(500) (or include delay(500) in Promise.all alongside
getRoomPlaying(parseInt(roomId))) so the resolve type is explicit
(Promise<void>) and the pattern is centralized for reuse.

106-108: useEffect ์˜์กด์„ฑ ๋ฐฐ์—ด์— navigate๊ฐ€ ๋ˆ„๋ฝ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

react-router-dom์˜ navigate๋Š” ์•ˆ์ •์ ์ธ ์ฐธ์กฐ์ด๋ฏ€๋กœ ์‹ค์ œ ๋ฒ„๊ทธ๊ฐ€ ๋˜์ง„ ์•Š์ง€๋งŒ, eslint-plugin-react-hooks์—์„œ ๊ฒฝ๊ณ ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ํŒŒ์ผ ๋‚ด์—์„œ navigate๋ฅผ ์‚ฌ์šฉํ•˜๋Š” useEffect์— ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”.

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/groupDetail/ParticipatedGroupDetail.tsx` around lines 106 - 108,
The useEffect that calls fetchRoomDetail currently lists [roomId] but omits the
navigate function, which can trigger eslint-plugin-react-hooks warnings; update
the dependency array for the useEffect that calls fetchRoomDetail to include
navigate (i.e., [roomId, navigate]) so the hook lists all referenced symbols
(fetchRoomDetail, navigate, roomId) and keeps hooks linting satisfied.

67-69: ๋‘ ๊ฐœ์˜ ๋’ค๋กœ๊ฐ€๊ธฐ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค โ€” ์˜๋„ํ•œ ๊ฒƒ์ธ์ง€ ํ™•์ธ ํ•„์š”.

handleBackClick์€ navigate(-1) (์—๋Ÿฌ ์ƒํƒœ์—์„œ ์‚ฌ์šฉ), handleBackButton์€ navigate('/group') (๋กœ๋”ฉ/๋ฉ”์ธ ์ฝ˜ํ…์ธ ์—์„œ ์‚ฌ์šฉ)์œผ๋กœ ๋™์ž‘์ด ๋‹ค๋ฆ…๋‹ˆ๋‹ค. ์—๋Ÿฌ ์‹œ ๋ธŒ๋ผ์šฐ์ € ํžˆ์Šคํ† ๋ฆฌ ๋’ค๋กœ๊ฐ€๊ธฐ, ์ •์ƒ ์‹œ ๊ทธ๋ฃน ๋ชฉ๋ก ์ด๋™์ด ์˜๋„๋œ ์„ค๊ณ„๋ผ๋ฉด ๊ดœ์ฐฎ์ง€๋งŒ, ์ผ๊ด€์„ฑ ์ธก๋ฉด์—์„œ ํ™•์ธํ•ด ์ฃผ์„ธ์š”.

Also applies to: 110-112

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/groupDetail/ParticipatedGroupDetail.tsx` around lines 67 - 69,
There are two different back handlersโ€”handleBackClick (calls navigate(-1)) and
handleBackButton (calls navigate('/group'))โ€”which may be unintended; decide on
the intended UX and make them consistent: either consolidate into a single
handler (e.g., unify both to navigate('/group') or to navigate(-1)) and replace
usages, or keep both but rename them to clarify intent (e.g.,
handleBackToHistory for navigate(-1) and handleBackToGroupList for
navigate('/group')) and ensure error and normal render paths call the correct
one; update any onClick references where handleBackClick or handleBackButton are
used so the code reflects the chosen behavior and add a short comment near the
handler(s) explaining when each should be used.
src/shared/ui/Skeleton/group/GroupDetailSkeleton.styled.ts (1)

4-14: SkeletonWrapper๊ฐ€ ParticipatedWrapper / Wrapper์™€ ๊ฑฐ์˜ ๋™์ผํ•œ ์Šคํƒ€์ผ์„ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

ParticipatedGroupDetail.styled.ts์˜ ParticipatedWrapper์™€ GroupDetail.styled.ts์˜ Wrapper ๋ชจ๋‘ ๋™์ผํ•œ ๋ ˆ์ด์•„์›ƒ ํŒจํ„ด(flex column, min/max-width, margin auto, background)์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๊ณตํ†ต ๋ž˜ํผ ์ปดํฌ๋„ŒํŠธ๋กœ ์ถ”์ถœํ•˜๋ฉด ์ค‘๋ณต์„ ์ค„์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/Skeleton/group/GroupDetailSkeleton.styled.ts` around lines 4 -
14, Duplicate layout styles exist across SkeletonWrapper, ParticipatedWrapper,
and Wrapper; extract a shared styled-component (e.g., CommonWrapper or
BaseLayout) that contains the shared rules (display:flex; position:relative;
flex-direction:column; align-items:center; min-width:320px; max-width:767px;
min-height:100vh; margin:0 auto; background-color:${colors.black.main}); replace
SkeletonWrapper, ParticipatedWrapper, and Wrapper to extend or wrap this new
CommonWrapper (via styled(CommonWrapper) or by composition) and keep any unique
styles in each specific component so duplication is removed but unique overrides
remain.
src/pages/searchBook/SearchBook.tsx (1)

82-87: minLoadingTime ํŒจํ„ด์ด ๋ฐ˜๋ณต์ ์œผ๋กœ ์‚ฌ์šฉ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ํŒŒ์ผ ๋‚ด์—์„œ ๋™์ผํ•œ new Promise(resolve => setTimeout(resolve, 500)) ํŒจํ„ด์ด ๋‘ ๋ฒˆ, ํ”„๋กœ์ ํŠธ ์ „์ฒด์—์„œ๋Š” ๋‹ค์ˆ˜ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ParticipatedGroupDetail.tsx ๋ฆฌ๋ทฐ์—์„œ ์–ธ๊ธ‰ํ•œ ๋Œ€๋กœ ๊ณตํ†ต ์œ ํ‹ธ๋กœ ์ถ”์ถœ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

Also applies to: 120-124

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/searchBook/SearchBook.tsx` around lines 82 - 87, Extract the
repeated new Promise(resolve => setTimeout(resolve, 500)) into a shared utility
(e.g., sleep or delay) and replace local minLoadingTime usages with that util;
specifically create a util function (sleep/msDelay) and import it into
SearchBook.tsx, then change the Promise.all call that currently awaits
getBookDetail(isbn), getRecruitingRooms(isbn), minLoadingTime to use sleep(500)
(and update the other occurrence around lines 120-124 similarly), and also
replace the same pattern in ParticipatedGroupDetail.tsx and other files to use
the new utility to avoid duplication.
๐Ÿค– Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/feed/MyFeed.tsx`:
- Around line 33-35: The current conditional in the MyFeed component (the if in
MyFeed that checks `loading || !profileData`) keeps the skeleton shown forever
when the profile API fails; change the logic so the skeleton is shown only while
`loading` is true (e.g., `if (loading) return <OtherFeedSkeleton .../>`), and
handle the `!profileData` case separately (render an error/fallback UI or an
empty-state with retry) once loading is false. Update the fetch handler in
MyFeed to set an explicit error or status flag (e.g., `profileError` or
`profileStatus`) so you can distinguish loading vs failed vs empty states and
render accordingly.

In `@src/pages/feed/FeedDetailPage.tsx`:
- Around line 69-74: The current Promise.all([...getFeedDetail, getComments,
minLoadingTime]) causes the 500ms minLoadingTime to be skipped if any API
rejects; extract minLoadingTime (const minLoadingTime = new Promise(resolve =>
setTimeout(resolve, 500))) outside Promise.all and ensure you always await it
even on error โ€” e.g., await Promise.all([getFeedDetail(...), getComments(...)])
inside a try/catch (or use Promise.allSettled for both API calls) and in a
finally block await minLoadingTime before updating state or hiding the skeleton;
update references to feedResponse/commentsResponse accordingly to handle
settled/rejected results.

In `@src/pages/feed/FollowerListPage.tsx`:
- Line 132: The TitleHeader usage is missing an alt on the leftArrow image:
update the leftIcon prop where TitleHeader is rendered (the JSX line using
leftArrow, handleBackClick, and title) to include an alt attribute on the <img>
(e.g., alt="๋’ค๋กœ๊ฐ€๊ธฐ" to match MyFeedPage's back icon) so the image is accessible.

In `@src/pages/groupDetail/GroupDetail.tsx`:
- Around line 132-141: The error block in GroupDetail.tsx returns plain div text
without a color, which can be invisible against the dark background; update the
error rendering to set an explicit text color (e.g., white or var(--color-red))
or reuse the existing ErrorContainer styled-component used in
ParticipatedGroupDetail so the message inside Wrapper/Header/IconButton (with
leftArrow and handleBackButton) is visible; replace the inline div with the
styled ErrorContainer (or add a color style) to ensure consistent error styling
similar to SearchBook.tsx and ParticipatedGroupDetail.
- Around line 314-316: The displayed date range in GroupDetail (MetaDate showing
roomData.progressStartDate ~ roomData.progressEndDate) uses raw API strings and
is inconsistent with ParticipatedGroupDetail which uses formatDate to convert
'-' to '.'; update GroupDetail to pass roomData.progressStartDate and
roomData.progressEndDate through the same formatDate utility (or the shared date
formatting helper used by ParticipatedGroupDetail) before rendering inside
MetaDate so both pages show the same formatted dates.

In `@src/pages/groupMembers/GroupMembers.tsx`:
- Around line 34-38: The current Promise.all between
getRoomMembers(parseInt(currentRoomId)) and minLoadingTime can short-circuit on
API failure and skip the 500ms delay; change the pattern so you await
getRoomMembers (or its try/catch) and always await minLoadingTime in a
.finally() (or use try { await getRoomMembers(...) } catch (err) {
store/propagate err } finally { await minLoadingTime }) to guarantee the minimum
loading time; ensure you reference getRoomMembers and minLoadingTime and rethrow
or handle the error after the finally so existing error handling/upstream
behavior (response variable usage) remains correct.

In `@src/pages/groupSearch/GroupSearch.tsx`:
- Around line 359-366: The delete callback (handleDelete) may throw because
deleteRecentSearch can throw on failure; wrap the async call in a try/catch
inside handleDelete, locate the handler using handleDelete and
deleteRecentSearch (and recentSearches/fetchRecentSearches) to add error
handling, await deleteRecentSearch inside try, keep the existing res.isSuccess
flow to call fetchRecentSearches on success, and in catch log the error and/or
show a user-facing notification so the rejection is not unhandled.

In `@src/pages/memory/Memory.tsx`:
- Around line 318-342: When rendering the loading skeleton, pointerEvents:
'none' still allows keyboard focus and Enter/Space to trigger handleTabChange
and change activeTab (causing duplicate loads); add the inert attribute to the
FixedSection wrapper when loading is true so all focus and keyboard interactions
are blocked (i.e., render <FixedSection inert ...> alongside the existing
style), and if FixedSection is a custom component ensure it forwards the inert
prop to the underlying DOM element (or explicitly set inert on that DOM node) so
RecordTabs, handleTabChange and RecordFilters cannot receive focus or fire
events during loading.

In `@src/pages/mypage/Mypage.tsx`:
- Around line 108-134: Currently the conditional `loading || !profile` causes
the skeleton to render even when loading is finished but profile fetch failed;
update the render logic in Mypage (around the JSX using ProfileSkeletonContainer
and UserProfile) to explicitly branch: if (loading) show the skeleton, else if
(profile) show the UserProfile block (using profile.profileImageUrl,
profile.nickname, profile.aliasColor, profile.aliasName and handleEditClick),
else show a failure/fallback UI (e.g., an error message or empty state) so the
skeleton is not shown indefinitely when profile is null and loading is false.

In `@src/pages/mypage/SavePage.tsx`:
- Around line 128-133: The current Promise.all call can reject early and bypass
minLoadingTime; change the flow so API calls and the minimum loading timer are
awaited independently: use Promise.allSettled([getSavedFeedsInMy(null),
getSavedBooksInMy()]) to collect both results without throwing, then await the
existing minLoadingTime Promise (minLoadingTime) before proceeding to process
the settled results (inspect the .status and .value/.reason for
feedsResponse/booksResponse) so the skeleton is guaranteed to show at least
500ms even if one request fails.

In `@src/pages/searchBook/SearchBook.tsx`:
- Around line 257-265: TopBackground is rendered with
bookImgUrl={bookDetail?.imageUrl || ''} even when bookDetail is null, causing an
empty url() CSS and unwanted requests; change the render logic so TopBackground
is only mounted when bookDetail is available (e.g., move <TopBackground ... />
inside the branch where !isLoading && bookDetail is truthy) or alternatively
update TopBackground to accept an optional prop and skip applying the background
when bookImgUrl is falsy; refer to TopBackground, bookDetail, bookImgUrl,
BookDetailSkeleton and isLoading to locate the relevant JSX and implement the
conditional rendering or guard.
- Around line 244-255: The error guard renders when error is truthy, so the '||
"์ฑ… ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."' fallback is dead code; change the JSX inside the if (error)
block to render the actual error only (e.g., replace {error || '์ฑ… ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜
์—†์Šต๋‹ˆ๋‹ค.'} with {error} or String(error)) so the Wrapper/Header/IconButton
(leftArrow, handleBackButton) block shows the real error; alternatively, if you
intended to show the fallback when no error exists, adjust the surrounding
condition (remove if (error) and render conditional content) instead.

In `@src/shared/ui/Skeleton/base/Skeleton.tsx`:
- Around line 62-85: The component currently renders an empty
SkeletonTextWrapper when lines is 0 or negative; update the guard in
Skeleton.Text so that values <= 1 render a single SkeletonTextLine instead of
the wrapper. Specifically, change the conditional that checks lines (currently
if (lines === 1)) to if (lines <= 1) and keep using
width/height/className/lastLineWidth appropriately when returning the single
SkeletonTextLine; this avoids rendering an empty SkeletonTextWrapper for lines
<= 0.

In `@src/shared/ui/Skeleton/group/GroupBookSectionSkeleton.tsx`:
- Around line 7-17: GroupBookSectionSkeleton currently sets only
cursor:'default' which still allows event propagation; update the component so
the container (StyledGroupBookSection) also applies pointerEvents:'none' when
rendering the skeleton (same behavior as BookItemSkeleton) to fully block clicks
and pointer events; locate GroupBookSectionSkeleton and add pointerEvents:'none'
to the inline style or the styled component variant used for skeleton rendering
so parent click handlers wonโ€™t fire.

In `@src/shared/ui/Skeleton/README.md`:
- Around line 25-50: Add blank lines before and after each Markdown table to
satisfy markdownlint MD058: insert an empty line immediately above and below the
table under the "### Skeleton (๊ธฐ๋ณธ)" section (the
width/height/borderRadius/className table), do the same for the "###
Skeleton.Circle" table, and for the "### Skeleton.Text" table; ensure each table
block is separated from surrounding text by a single blank line so the tables
for Skeleton (๊ธฐ๋ณธ), Skeleton.Circle, and Skeleton.Text are each preceded and
followed by an empty line.

In `@src/shared/ui/Skeleton/searchBook/BookDetailSkeleton.tsx`:
- Around line 31-40: The skeleton buttons (RecruitingGroupButton,
WritePostButton, SaveButton) still receive keyboard focus because only
pointerEvents is disabled; update these components in BookDetailSkeleton so they
are non-interactive to keyboard users by adding disabled (or tabIndex={-1}) to
each button instance and remove any conflicting interactive propsโ€”e.g., set
<RecruitingGroupButton disabled style=...>, <WritePostButton disabled ...>, and
ensure <SaveButton already disabled> includes tabIndex={-1} or likewise; keep
the visual skeleton styles but make their ARIA/DOM state explicitly
non-interactive.

---

Outside diff comments:
In `@src/pages/feed/FollowerListPage.tsx`:
- Around line 61-70: Move the null-check for response/response.data before any
access to response.data to avoid runtime errors: check if response and
response.data are truthy (e.g., if (!response || !response.data) { setError('API
์‘๋‹ต์ด ์—†์Šต๋‹ˆ๋‹ค.'); return; }) before computing userData, then populate userData using
the existing type switch that references (response.data as { followers:
FollowData[] }) or followings; alternatively replace direct access with safe
optional chaining when assigning userData (e.g., response?.data?.followers) but
ensure setError/return still runs when data is missing; update the logic around
the variables response, userData, type and setError in FollowerListPage.tsx
accordingly.

In `@src/pages/feed/MyFeedPage.tsx`:
- Around line 75-77: The current MyFeedPage returns an empty fragment on error
(if (error) return <></>), leaving users stranded; replace that with the same
error UI + back navigation used in GroupDetail/ParticipatedGroupDetail: render a
user-friendly error message and a visible "Back" control that triggers the page
navigation (e.g., the same BackButton or useNavigate/navigation callback used
elsewhere) and preserve any error details for logging or debugging; update the
error branch inside the MyFeedPage component to mirror the error-handling
pattern from GroupDetail/ParticipatedGroupDetail instead of returning an empty
fragment.

In `@src/pages/groupDetail/GroupDetail.tsx`:
- Around line 55-57: The isLoading state in GroupDetail (useState for isLoading)
is initialized to false which disagrees with ParticipatedGroupDetail; change the
initial value to true so the first render reflects loading semantics (update the
useState<boolean> call that declares isLoading and setIsLoading in
GroupDetail.tsx to use true) to keep behavior consistent with
ParticipatedGroupDetail and avoid a non-loading initial render before
setIsLoading(true) runs.

In `@src/pages/groupDetail/ParticipatedGroupDetail.tsx`:
- Around line 56-63: The ParticipatedGroupDetail component initializes loading
as true which immediately shows the skeleton on first render; make the initial
loading value consistent with GroupDetail.tsx by setting the loading state
(const [loading, setLoading]) to false (or align both components' intent) and
rely on the !roomData guard for initial empty-render behavior, updating
setLoading(true/false) only around your async fetch logic (functions referenced:
ParticipatedGroupDetail, setLoading, roomData) so both pages behave
consistently.

In `@src/pages/groupSearch/GroupSearch.tsx`:
- Around line 50-59: The useEffect hooks call fetchRecentSearches() twice on
mount because searchStatus starts as 'idle'โ€”fix by consolidating the logic: keep
a single useEffect (the one with empty deps or the one watching searchStatus)
and add a guard so fetchRecentSearches() is invoked only when searchStatus ===
'idle' (or remove the effect that watches searchStatus entirely and rely on the
mount effect with the status check); update references to useEffect,
fetchRecentSearches, and searchStatus accordingly to avoid duplicate initial
requests and loading flicker.

---

Duplicate comments:
In `@src/components/search/MostSearchedBooks.tsx`:
- Around line 30-34: The Promise.all usage with getMostSearchedBooks and
minLoadingTime can reject immediately when the API fails, breaking the
guaranteed skeleton display; change the logic to await both promises regardless
of API failure (e.g., use Promise.allSettled([getMostSearchedBooks(),
minLoadingTime]) and then extract the fulfilled value from the first result or
fall back to safe empty data), or wrap getMostSearchedBooks() with a catch that
returns a sentinel/fallback so Promise.all always resolves; update references in
this component (minLoadingTime, getMostSearchedBooks) accordingly.

In `@src/pages/feed/OtherFeedPage.tsx`:
- Around line 37-46: The current Promise.all with getOtherFeed, getOtherProfile
and minLoadingTime can fail-fast and skip the 500ms guarantee; wrap the API
calls so the timing promise always resolves independentlyโ€”e.g., use
Promise.allSettled or attach .catch handlers to getOtherFeed/getOtherProfile
before awaitingโ€”then after awaiting, check results and throw or handle errors
and extract values into feedResponse/profileResponse; update the block
referencing getOtherFeed, getOtherProfile, minLoadingTime, feedResponse and
profileResponse accordingly.

In `@src/pages/today-words/TodayWords.tsx`:
- Around line 94-101: The current Promise.all usage with minLoadingTime plus
getDailyGreeting means if getDailyGreeting rejects the 500ms delay may be
skipped; change to ensure minLoadingTime is always awaited by either awaiting
minLoadingTime in a finally block or by converting the API call into a
non-rejecting promise and then using Promise.all/Promise.allSettled. Concretely,
wrap getDailyGreeting(roomId: parseInt(roomId), cursor: cursor || undefined)
into a promise that catches errors (so it never rejects) or use
Promise.allSettled for [getDailyGreeting(...), minLoadingTime] and then handle
the settled result for the API call (check for rejection and process
accordingly); update references in TodayWords to use the new safe API result
handling and keep isRefresh logic the same.

---

Nitpick comments:
In `@src/pages/feed/MyFeedPage.tsx`:
- Around line 38-46: The current Promise.all includes minLoadingTime in the same
array which forces a manual cast and risks silent runtime bugs if array order
changes; change the code to await the two network calls together and await
minLoadingTime separately: call Promise.all([getOtherFeed(Number(userId)),
getOtherProfile(Number(userId))]) to get [feedResponse, profileResponse] (no
manual cast), then await minLoadingTime on its own (e.g., await minLoadingTime)
before continuing; update references to feedResponse and profileResponse
accordingly.

In `@src/pages/groupDetail/ParticipatedGroupDetail.tsx`:
- Around line 80-86: Replace the inline new Promise pattern used as
minLoadingTime with a reusable typed utility (e.g., export const delay = (ms:
number): Promise<void>) and use it in the ParticipatedGroupDetail component when
awaiting getRoomPlaying; specifically, remove new Promise(resolve =>
setTimeout(resolve, 500)) and call await delay(500) (or include delay(500) in
Promise.all alongside getRoomPlaying(parseInt(roomId))) so the resolve type is
explicit (Promise<void>) and the pattern is centralized for reuse.
- Around line 106-108: The useEffect that calls fetchRoomDetail currently lists
[roomId] but omits the navigate function, which can trigger
eslint-plugin-react-hooks warnings; update the dependency array for the
useEffect that calls fetchRoomDetail to include navigate (i.e., [roomId,
navigate]) so the hook lists all referenced symbols (fetchRoomDetail, navigate,
roomId) and keeps hooks linting satisfied.
- Around line 67-69: There are two different back handlersโ€”handleBackClick
(calls navigate(-1)) and handleBackButton (calls navigate('/group'))โ€”which may
be unintended; decide on the intended UX and make them consistent: either
consolidate into a single handler (e.g., unify both to navigate('/group') or to
navigate(-1)) and replace usages, or keep both but rename them to clarify intent
(e.g., handleBackToHistory for navigate(-1) and handleBackToGroupList for
navigate('/group')) and ensure error and normal render paths call the correct
one; update any onClick references where handleBackClick or handleBackButton are
used so the code reflects the chosen behavior and add a short comment near the
handler(s) explaining when each should be used.

In `@src/pages/mypage/Mypage.tsx`:
- Around line 109-118: The markup duplicated in Mypage.tsx
(ProfileSkeletonContainer / ProfileSkeletonLeft / ProfileSkeletonText with
Skeleton.Circle/Text/Box) should be replaced by the shared ProfileSkeleton
component from src/shared/ui/Skeleton/feed/ProfileSkeleton.tsx; locate the block
that renders ProfileSkeletonContainer and swap it for the shared
<ProfileSkeleton /> (or pass the existing size/variant props if ProfileSkeleton
supports them) so styling and behavior come from the single source of truth and
the local skeleton subcomponents (ProfileSkeletonContainer, ProfileSkeletonLeft,
ProfileSkeletonText, Skeleton.Circle, Skeleton.Text, Skeleton.Box) are removed
from this file.

In `@src/pages/searchBook/SearchBook.tsx`:
- Around line 82-87: Extract the repeated new Promise(resolve =>
setTimeout(resolve, 500)) into a shared utility (e.g., sleep or delay) and
replace local minLoadingTime usages with that util; specifically create a util
function (sleep/msDelay) and import it into SearchBook.tsx, then change the
Promise.all call that currently awaits getBookDetail(isbn),
getRecruitingRooms(isbn), minLoadingTime to use sleep(500) (and update the other
occurrence around lines 120-124 similarly), and also replace the same pattern in
ParticipatedGroupDetail.tsx and other files to use the new utility to avoid
duplication.

In `@src/shared/ui/Skeleton/base/Skeleton.styled.ts`:
- Around line 54-56: SkeletonTextLine duplicates the height interpolation
already handled by SkeletonBase; remove the redundant height handling from
SkeletonTextLine and instead rely on SkeletonBase's logic while only overriding
the default value (from 20px to 16px) via a single source of truthโ€”e.g., pass a
default prop or theme/variable used by SkeletonBaseโ€”so update SkeletonTextLine
to stop re-declaring height and supply the desired default height through
SkeletonBase's prop/default mechanism (reference SkeletonTextLine and
SkeletonBase and the height prop).
- Around line 22-36: The SkeletonBase styled component currently uses display:
inline-block which can cause multiple skeletons to flow horizontally in
non-flex/grid parents; update SkeletonBase to use display: block so it behaves
as a predictable full-width block element in plain containers, keeping the
existing width/height/borderRadius logic and animations intact (locate
SkeletonBase in src/shared/ui/Skeleton/base/Skeleton.styled.ts and replace the
display rule from inline-block to block).

In `@src/shared/ui/Skeleton/base/Skeleton.tsx`:
- Around line 40-44: The base and box APIs are redundant: SkeletonBox is just a
styled(SkeletonBase) empty extension and Skeleton.Box duplicates functionality;
make the rectangular API explicit by keeping Skeleton.Box (Skeleton.Box
component) as the public export and restrict the base Skeleton (Skeleton and
SkeletonBase) to internal use onlyโ€”remove or stop exporting the top-level
Skeleton component, keep SkeletonBox/Skeleton.Box as the single public rectangle
API, update any export lists and consume-sites to import Skeleton.Box instead of
Skeleton, and add a deprecation note/alias if needed to preserve backwards
compatibility.

In `@src/shared/ui/Skeleton/group/BannerSkeleton.tsx`:
- Around line 15-20: The inline marginTop in BannerSkeleton.tsx should be moved
to a styled-component in GroupDetailSkeleton.styled.ts for consistency; create a
new styled export (e.g., BannerSkeletonWrapper or reuse an existing pattern like
MetaSkeletonRow/TagSkeletonRow), apply the margin-top: 20px (and the inner 8px
spacer as another styled div or apply a margin-top on a new InnerWrapper),
export and import that styled into BannerSkeleton.tsx, then replace the two
inline-styled divs with the new styled components so no inline styles remain.

In `@src/shared/ui/Skeleton/group/GroupDetailSkeleton.styled.ts`:
- Around line 4-14: Duplicate layout styles exist across SkeletonWrapper,
ParticipatedWrapper, and Wrapper; extract a shared styled-component (e.g.,
CommonWrapper or BaseLayout) that contains the shared rules (display:flex;
position:relative; flex-direction:column; align-items:center; min-width:320px;
max-width:767px; min-height:100vh; margin:0 auto;
background-color:${colors.black.main}); replace SkeletonWrapper,
ParticipatedWrapper, and Wrapper to extend or wrap this new CommonWrapper (via
styled(CommonWrapper) or by composition) and keep any unique styles in each
specific component so duplication is removed but unique overrides remain.

In `@src/shared/ui/Skeleton/group/HotTopicSectionSkeleton.tsx`:
- Around line 15-22: HotTopicSectionSkeleton currently uses inline style objects
on the outer wrapper and two inner divs; extract those into styled-components
(e.g., create StyledHotTopicWrapper, StyledTitleWrapper, StyledListWrapper in
HotTopicSectionSkeleton.styled.ts or reuse GroupDetailSkeleton.styled) and
replace the inline divs in the HotTopicSectionSkeleton component with these
styled components, preserving the same CSS (padding: 16px 0; margin-bottom:
16px; and display:flex; flex-direction:column; gap:8px;) and keep existing
Skeleton children and props unchanged.

In `@src/shared/ui/Skeleton/members/MemberListSkeleton.tsx`:
- Around line 16-18: Remove the unnecessary wrapper <div> around Skeleton.Circle
in the MemberListSkeleton component: because ProfileSection already applies
display:flex, align-items:center and gap:8px, place Skeleton.Circle directly as
a child (remove the extra div node) so the DOM is simpler and layout remains
unchanged; update the JSX in MemberListSkeleton (where Skeleton.Circle is
rendered) to render Skeleton.Circle inline and verify styling via ProfileSection
still centers and gaps correctly.

In `@src/shared/ui/Skeleton/memory/RecordItemSkeleton.tsx`:
- Around line 12-36: The three repeated inline divs inside the
RecordItemSkeleton (the ones wrapping Skeleton.Box + Skeleton.Text in
ActionSection and the circle wrapper in UserSection) should be replaced with a
lightweight styled component to remove duplicated inline styles; create a styled
component (e.g., IconRow or InlineRow) and use it in place of the divs in
Container/UserSection/ActionSection so styles like display:flex, gap, alignItems
and marginRight are centralized and applied consistently across the component.

In `@src/shared/ui/Skeleton/search/RecentSearchTabsSkeleton.tsx`:
- Around line 4-15: RecentSearchTabsSkeleton currently hardcodes the Title text
("์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด"), causing drift if RecentSearchTabs' title changes; update
RecentSearchTabsSkeleton to avoid literal text by either rendering the same
shared constant used by RecentSearchTabs (import the title constant) or
rendering a Skeleton placeholder for the title (replace Title's literal children
with a Skeleton.Box/Skeleton.Text), targeting the RecentSearchTabsSkeleton
component and the Title element so the skeleton stays in sync with the real
component.

In `@src/shared/ui/Skeleton/searchBook/BookItemSkeleton.tsx`:
- Around line 18-23: ํ˜„์žฌ BookItemSkeleton.tsx์—์„œ BookInfo ๋‚ด๋ถ€์— bare <div style={{
marginTop: '8px' }}>๋กœ ์ธ๋ผ์ธ ์Šคํƒ€์ผ์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์–ด styled-component ํŒจํ„ด๊ณผ ๋ถˆ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค; BookInfo ์•ˆ์˜
์„ธ ๋ฒˆ์งธ Skeleton.Text ์ƒ๋‹จ ์—ฌ๋ฐฑ์„ ์œ„ํ•ด inline div๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  ๋Œ€์‹  Skeleton.Text์— margin/top ์Šคํƒ€์ผ์„
์ „๋‹ฌํ•˜๊ฑฐ๋‚˜(๋งŒ์•ฝ Skeleton.Text๊ฐ€ style prop์„ ์ง€์›ํ•˜์ง€ ์•Š์œผ๋ฉด SkeletonProps์— style?:
React.CSSProperties๋ฅผ ์ถ”๊ฐ€) ๋˜๋Š” BookInfo ๋‚ด๋ถ€์— ๋ž˜ํผ styled-component(์˜ˆ: InfoGap ๋˜๋Š”
StyledSkeletonWrap)๋ฅผ ๋งŒ๋“ค์–ด margin-top์„ ์ ์šฉํ•˜๋„๋ก ๋ณ€๊ฒฝํ•˜์„ธ์š”; ์ฐธ์กฐํ•  ์‹๋ณ„์ž๋Š” BookItemSkeleton.tsx,
BookInfo, Skeleton.Text์ž…๋‹ˆ๋‹ค.

โ„น๏ธ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled

You can enable these sources in your CodeRabbit configuration.

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between fa6d152 and 56a615e.

๐Ÿ“’ Files selected for processing (61)
  • src/components/feed/MyFeed.tsx
  • src/components/search/MostSearchedBooks.tsx
  • src/pages/feed/Feed.styled.ts
  • src/pages/feed/Feed.tsx
  • src/pages/feed/FeedDetailPage.styled.ts
  • src/pages/feed/FeedDetailPage.tsx
  • src/pages/feed/FollowerListPage.tsx
  • src/pages/feed/MyFeedPage.tsx
  • src/pages/feed/OtherFeedPage.tsx
  • src/pages/groupDetail/GroupDetail.tsx
  • src/pages/groupDetail/ParticipatedGroupDetail.tsx
  • src/pages/groupMembers/GroupMembers.tsx
  • src/pages/groupSearch/GroupSearch.tsx
  • src/pages/memory/Memory.tsx
  • src/pages/mypage/Mypage.styled.ts
  • src/pages/mypage/Mypage.tsx
  • src/pages/mypage/SavePage.styled.ts
  • src/pages/mypage/SavePage.tsx
  • src/pages/search/Search.tsx
  • src/pages/searchBook/SearchBook.tsx
  • src/pages/today-words/TodayWords.tsx
  • src/shared/ui/Skeleton/README.md
  • src/shared/ui/Skeleton/base/Skeleton.styled.ts
  • src/shared/ui/Skeleton/base/Skeleton.tsx
  • src/shared/ui/Skeleton/base/index.ts
  • src/shared/ui/Skeleton/feed/FeedPostSkeleton.styled.ts
  • src/shared/ui/Skeleton/feed/FeedPostSkeleton.tsx
  • src/shared/ui/Skeleton/feed/OtherFeedSkeleton.styled.ts
  • src/shared/ui/Skeleton/feed/OtherFeedSkeleton.tsx
  • src/shared/ui/Skeleton/feed/ProfileSkeleton.styled.ts
  • src/shared/ui/Skeleton/feed/ProfileSkeleton.tsx
  • src/shared/ui/Skeleton/feed/TotalBarSkeleton.styled.ts
  • src/shared/ui/Skeleton/feed/TotalBarSkeleton.tsx
  • src/shared/ui/Skeleton/feed/UserProfileItemSkeleton.tsx
  • src/shared/ui/Skeleton/feed/index.ts
  • src/shared/ui/Skeleton/group/BannerSkeleton.tsx
  • src/shared/ui/Skeleton/group/BookSkeleton.tsx
  • src/shared/ui/Skeleton/group/CommentSectionSkeleton.tsx
  • src/shared/ui/Skeleton/group/GroupBookSectionSkeleton.tsx
  • src/shared/ui/Skeleton/group/GroupCardSkeleton.tsx
  • src/shared/ui/Skeleton/group/GroupDetailSkeleton.styled.ts
  • src/shared/ui/Skeleton/group/GroupDetailSkeleton.tsx
  • src/shared/ui/Skeleton/group/HotTopicSectionSkeleton.tsx
  • src/shared/ui/Skeleton/group/RecordSectionSkeleton.tsx
  • src/shared/ui/Skeleton/group/index.ts
  • src/shared/ui/Skeleton/index.ts
  • src/shared/ui/Skeleton/members/MemberListSkeleton.tsx
  • src/shared/ui/Skeleton/members/index.ts
  • src/shared/ui/Skeleton/memory/RecordItemSkeleton.tsx
  • src/shared/ui/Skeleton/memory/index.ts
  • src/shared/ui/Skeleton/search/MostSearchedBooksSkeleton.tsx
  • src/shared/ui/Skeleton/search/RecentSearchTabsSkeleton.tsx
  • src/shared/ui/Skeleton/search/index.ts
  • src/shared/ui/Skeleton/searchBook/BookDetailSkeleton.tsx
  • src/shared/ui/Skeleton/searchBook/BookItemSkeleton.tsx
  • src/shared/ui/Skeleton/searchBook/index.ts
  • src/shared/ui/Skeleton/todaywords/DateDividerSkeleton.tsx
  • src/shared/ui/Skeleton/todaywords/MessageItemSkeleton.tsx
  • src/shared/ui/Skeleton/todaywords/MessageListSkeleton.styled.ts
  • src/shared/ui/Skeleton/todaywords/MessageListSkeleton.tsx
  • src/shared/ui/Skeleton/todaywords/index.ts

Comment thread src/components/feed/MyFeed.tsx Outdated
Comment on lines +69 to 74
const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500));
const [feedResponse, commentsResponse] = await Promise.all([
getFeedDetail(Number(feedId)),
getComments(Number(feedId), { postType: 'FEED' }),
minLoadingTime,
]);

@coderabbitai coderabbitai Bot Feb 25, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸก Minor

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

# FeedDetailPage.tsx ํŒŒ์ผ ํ™•์ธ
fd "FeedDetailPage.tsx" --type f

Repository: THIP-TextHip/THIP-Web

Length of output: 98


๐Ÿ Script executed:

# ํŒŒ์ผ ํฌ๊ธฐ ํ™•์ธ
wc -l src/pages/feed/FeedDetailPage.tsx

Repository: THIP-TextHip/THIP-Web

Length of output: 102


๐Ÿ Script executed:

# FeedDetailPage.tsx ํŒŒ์ผ ๋‚ด์šฉ ํ™•์ธ
cat -n src/pages/feed/FeedDetailPage.tsx

Repository: THIP-TextHip/THIP-Web

Length of output: 8364


์‹คํŒจ ๊ฒฝ๋กœ์—์„œ ์ตœ์†Œ ๋กœ๋”ฉ ์‹œ๊ฐ„์ด ๋ณด์žฅ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

69-74์ค„์˜ Promise.all ๋‚ด๋ถ€์— minLoadingTime์„ ํฌํ•จํ•˜๋ฉด, API ์ค‘ ํ•˜๋‚˜๊ฐ€ ์‹คํŒจํ•  ๋•Œ Promise.all์ด ์ฆ‰์‹œ ๊ฑฐ๋ถ€๋˜์–ด 500ms๋ฅผ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์—๋Ÿฌ ์ผ€์ด์Šค์—์„œ๋„ ์ตœ์†Œ 500ms ๋™์•ˆ skeleton์„ ํ‘œ์‹œํ•˜๋ ค๋ฉด minLoadingTime์„ Promise.all ์™ธ๋ถ€๋กœ ๋ถ„๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ˆ˜์ • ์˜ˆ์‹œ
        const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500));
-       const [feedResponse, commentsResponse] = await Promise.all([
+       const [feedResponse, commentsResponse] = await Promise.all([
          getFeedDetail(Number(feedId)),
          getComments(Number(feedId), { postType: 'FEED' }),
-         minLoadingTime,
-       ]);
+       ]);
+       await minLoadingTime;
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/feed/FeedDetailPage.tsx` around lines 69 - 74, The current
Promise.all([...getFeedDetail, getComments, minLoadingTime]) causes the 500ms
minLoadingTime to be skipped if any API rejects; extract minLoadingTime (const
minLoadingTime = new Promise(resolve => setTimeout(resolve, 500))) outside
Promise.all and ensure you always await it even on error โ€” e.g., await
Promise.all([getFeedDetail(...), getComments(...)]) inside a try/catch (or use
Promise.allSettled for both API calls) and in a finally block await
minLoadingTime before updating state or hiding the skeleton; update references
to feedResponse/commentsResponse accordingly to handle settled/rejected results.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

#!/bin/bash
# Fetch the commit details to see what changes were made
gh api repos/THIP-TextHip/THIP-Web/commits/20856eb6d329bed7f2639945492053acf678f755 --jq '.commit.message, .files[] | select(.filename == "src/pages/feed/FeedDetailPage.tsx") | .patch'

Length of output: 57


๐Ÿ Script executed:

#!/bin/bash
# Fetch the commit message
echo "=== Commit Message ==="
gh api repos/THIP-TextHip/THIP-Web/commits/20856eb6d329bed7f2639945492053acf678f755 --jq '.commit.message'

echo -e "\n=== Changes to FeedDetailPage.tsx ==="
# Fetch the patch for FeedDetailPage.tsx
gh api repos/THIP-TextHip/THIP-Web/commits/20856eb6d329bed7f2639945492053acf678f755 --jq '.files[] | select(.filename == "src/pages/feed/FeedDetailPage.tsx") | .patch'

Length of output: 499


@ljh130334 ์ปค๋ฐ‹ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค! ์„ฑ๊ณต ๊ฒฝ๋กœ์— ๋Œ€ํ•œ ๊ฐœ์„ ์€ ์ž˜ ๋˜์—ˆ์ง€๋งŒ, ์—๋Ÿฌ ๊ฒฝ๋กœ์—์„œ ์—ฌ์ „ํžˆ ์ตœ์†Œ ๋กœ๋”ฉ ์‹œ๊ฐ„์ด ๋ณด์žฅ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

ํ˜„์žฌ ์ฝ”๋“œ ํ๋ฆ„์„ ๋ณด๋ฉด:

  • โœ… ์„ฑ๊ณต ์‹œ: Promise.all ์™„๋ฃŒ โ†’ await minLoadingTime ๋Œ€๊ธฐ โ†’ ๋ฐ์ดํ„ฐ ์„ค์ •
  • โŒ ์‹คํŒจ ์‹œ: Promise.all ์ฆ‰์‹œ reject โ†’ catch ๋ธ”๋ก์œผ๋กœ ์ ํ”„ (69์ค„์˜ await minLoadingTime์„ ๊ฑด๋„ˆ๋œ€)

API๊ฐ€ 100ms๋งŒ์— ์‹คํŒจํ•˜๋ฉด skeleton์ด 100ms๋งŒ ํ‘œ์‹œ๋˜๊ณ  ๋ฐ”๋กœ ์—๋Ÿฌ ์ƒํƒœ๋กœ ์ „ํ™˜๋ฉ๋‹ˆ๋‹ค.

์™„์ „ํ•œ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•:

      try {
        setLoading(true);

        const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500));
        const [feedResponse, commentsResponse] = await Promise.all([
          getFeedDetail(Number(feedId)),
          getComments(Number(feedId), { postType: 'FEED' }),
        ]);
        await minLoadingTime;

        setFeedData(feedResponse.data);
        setCommentList(commentsResponse.data.commentList);
        setError(null);
      } catch (err) {
+       await minLoadingTime;
        console.error('ํ”ผ๋“œ ์ƒ์„ธ ์ •๋ณด ๋˜๋Š” ๋Œ“๊ธ€ ๋กœ๋“œ ์‹คํŒจ:', err);
        setError('ํ”ผ๋“œ ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
      } finally {
        setLoading(false);
      }

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•ด๋„ ์ตœ์†Œ 500ms ๋™์•ˆ skeleton์„ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Comment thread src/pages/feed/FollowerListPage.tsx Outdated
Comment thread src/pages/groupDetail/GroupDetail.tsx
Comment thread src/pages/groupDetail/GroupDetail.tsx
Comment thread src/pages/searchBook/SearchBook.tsx
Comment thread src/shared/ui/Skeleton/base/Skeleton.tsx
Comment thread src/shared/ui/Skeleton/group/GroupBookSectionSkeleton.tsx
Comment thread src/shared/ui/Skeleton/README.md
Comment thread src/shared/ui/Skeleton/searchBook/BookDetailSkeleton.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and canโ€™t be posted inline due to platform limitations.

โš ๏ธ Outside diff range comments (1)
src/pages/groupSearch/GroupSearch.tsx (1)

50-59: โš ๏ธ Potential issue | ๐ŸŸก Minor

๋งˆ์šดํŠธ ์‹œ ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ์กฐํšŒ๊ฐ€ ์ค‘๋ณต ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.

Line [51]๊ณผ Line [56]์—์„œ ์ดˆ๊ธฐ ๋ Œ๋” ์‹œ fetchRecentSearches๊ฐ€ 2๋ฒˆ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ๋ถˆํ•„์š”ํ•œ ๋„คํŠธ์›Œํฌ ํ˜ธ์ถœ/์ƒํƒœ ๊ฒฝํ•ฉ์„ ์ค„์ด๊ธฐ ์œ„ํ•ด ํ•œ ๊ฒฝ๋กœ๋กœ ํ†ตํ•ฉํ•˜๋Š” ๊ฒŒ ์ข‹์Šต๋‹ˆ๋‹ค.

๐Ÿ”ง ์ œ์•ˆ ์ˆ˜์ •์•ˆ
-  useEffect(() => {
-    fetchRecentSearches();
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, []);
-
   useEffect(() => {
     if (searchStatus === 'idle') {
       fetchRecentSearches();
     }
   }, [searchStatus]);
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/groupSearch/GroupSearch.tsx` around lines 50 - 59,
fetchRecentSearches is being invoked twice on mount because there's a useEffect
with an empty dependency array calling fetchRecentSearches and a second
useEffect that calls fetchRecentSearches when searchStatus === 'idle', causing
duplicate network calls; consolidate into a single control path by removing the
unconditional mount call and relying on the searchStatus-driven effect (or vice
versa) โ€” update the effects around fetchRecentSearches and searchStatus so only
one effect (either the empty-deps useEffect or the [searchStatus] useEffect that
checks for 'idle') triggers the initial fetch to eliminate the duplicate call.
โ™ป๏ธ Duplicate comments (3)
src/pages/searchBook/SearchBook.tsx (2)

247-258: if (error) ๋ธ”๋ก ๋‚ด๋ถ€์˜ ํด๋ฐฑ ๋ฌธ์ž์—ด์ด ๋„๋‹ฌ ๋ถˆ๊ฐ€๋Šฅํ•œ dead code์ž…๋‹ˆ๋‹ค.

Line 254์—์„œ error๋Š” ์ด๋ฏธ truthy๊ฐ€ ๋ณด์žฅ๋˜๋ฏ€๋กœ || '์ฑ… ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.'๋Š” ์ ˆ๋Œ€ ์‹คํ–‰๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๐Ÿงน ์ œ์•ˆ
-          {error || '์ฑ… ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.'}
+          {error}
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/searchBook/SearchBook.tsx` around lines 247 - 258, The JSX inside
the if (error) branch contains a dead fallback string ("|| '์ฑ… ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.'")
because the clause only runs when error is truthy; update the rendering in
SearchBook.tsx to remove the unreachable fallback or change the condition/render
logic so the fallback is shown when error is falsyโ€”e.g., either remove "|| '์ฑ…
์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.'" from the block that returns Wrapper/Header/IconButton (using
symbols error, Wrapper, Header, IconButton, leftArrow, handleBackButton), or
replace the top-level conditional with a ternary (error ? error : '์ฑ… ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜
์—†์Šต๋‹ˆ๋‹ค.') so the fallback can be reached.

260-262: bookDetail์ด null์ผ ๋•Œ TopBackground๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด URL๋กœ ๋ Œ๋”๋ง๋ฉ๋‹ˆ๋‹ค.

bookDetail?.imageUrl || ''๋Š” ๋กœ๋”ฉ ์ค‘์— ๋นˆ url("") CSS๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ๋ถˆํ•„์š”ํ•œ ๋„คํŠธ์›Œํฌ ์š”์ฒญ์ด๋‚˜ ์ฝ˜์†” ๊ฒฝ๊ณ ๋ฅผ ์œ ๋ฐœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

โ™ป๏ธ ์ œ์•ˆ: ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง
-      <TopBackground bookImgUrl={bookDetail?.imageUrl || ''} />
+      {bookDetail && <TopBackground bookImgUrl={bookDetail.imageUrl} />}
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/searchBook/SearchBook.tsx` around lines 260 - 262, The
TopBackground is currently rendered with bookImgUrl={bookDetail?.imageUrl ||
''}, causing an empty url("") when bookDetail is null; change this to
conditionally render TopBackground only when bookDetail?.imageUrl is truthy (use
a guard around the TopBackground component) so you pass a real image URL
(bookDetail.imageUrl) or avoid rendering it entirely when absent; update the JSX
near Wrapper/TopBackground references to use this conditional rendering pattern
to prevent empty CSS urls and unnecessary network/console noise.
src/pages/groupSearch/GroupSearch.tsx (1)

380-387: โš ๏ธ Potential issue | ๐ŸŸก Minor

์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ์‚ญ์ œ ์ฝœ๋ฐฑ์— ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

Line [383]์˜ deleteRecentSearch๊ฐ€ throwํ•  ๊ฒฝ์šฐ ํ˜„์žฌ ํ•ธ๋“ค๋Ÿฌ์—์„œ unhandled rejection์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”ง ์ œ์•ˆ ์ˆ˜์ •์•ˆ
                 handleDelete={async (term: string) => {
                   const x = recentSearches.find(i => i.searchTerm === term);
                   if (!x) return;
-                  const res = await deleteRecentSearch(x.recentSearchId);
-                  if (res.isSuccess) {
-                    await fetchRecentSearches();
+                  try {
+                    const res = await deleteRecentSearch(x.recentSearchId);
+                    if (res.isSuccess) {
+                      await fetchRecentSearches();
+                    }
+                  } catch (error) {
+                    console.error('์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ์‚ญ์ œ ์˜ค๋ฅ˜:', error);
                   }
                 }}
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/groupSearch/GroupSearch.tsx` around lines 380 - 387, The
handleDelete callback currently calls deleteRecentSearch asynchronously without
catching exceptions; wrap the async body of handleDelete in a try/catch so any
thrown error from deleteRecentSearch (or subsequent await fetchRecentSearches)
is caught and handled (e.g., log via console/error logger and optionally surface
a user-facing error), ensure you still return/exit early if
recentSearches.find(...) returns undefined, and keep calling fetchRecentSearches
only on successful delete responses (res.isSuccess) inside the try block to
avoid unhandled promise rejections.
๐Ÿงน Nitpick comments (1)
src/pages/searchBook/SearchBook.tsx (1)

84-89: minLoadingTime์— ๋ช…์‹œ์  Promise<void> ํƒ€์ž…์„ ์ง€์ •ํ•˜์„ธ์š”.

new Promise(resolve => setTimeout(resolve, 500))๋Š” TypeScript๊ฐ€ Promise<unknown>์œผ๋กœ ์ถ”๋ก ํ•ฉ๋‹ˆ๋‹ค. ์˜๋„๋ฅผ ๋ช…ํ™•ํžˆ ํ•˜๊ณ  Promise.all์˜ ๋ฐ˜ํ™˜ ํƒ€์ž… ์ถ”๋ก ์„ ๊น”๋”ํ•˜๊ฒŒ ์œ ์ง€ํ•˜๋ ค๋ฉด Promise<void>๋ฅผ ๋ช…์‹œํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

โ™ป๏ธ ์ œ์•ˆ: ๋ช…์‹œ์  ํƒ€์ž… ์–ด๋…ธํ…Œ์ด์…˜
-        const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500));
+        const minLoadingTime = new Promise<void>(resolve => setTimeout(resolve, 500));

(Line 120 ๋™์ผํ•˜๊ฒŒ ์ ์šฉ)

-      const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500));
+      const minLoadingTime = new Promise<void>(resolve => setTimeout(resolve, 500));

Also applies to: 120-124

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/searchBook/SearchBook.tsx` around lines 84 - 89, The temporary
loading timer promise minLoadingTime is inferred as Promise<unknown>; update its
declaration to explicitly type it as Promise<void> (e.g., const minLoadingTime:
Promise<void> = new Promise(resolve => setTimeout(resolve, 500))); do the same
for the other instance around lines 120-124 so Promise.all([getBookDetail(isbn),
getRecruitingRooms(isbn), minLoadingTime]) keeps correct type inference for
getBookDetail and getRecruitingRooms.
๐Ÿค– Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/pages/groupSearch/GroupSearch.tsx`:
- Around line 50-59: fetchRecentSearches is being invoked twice on mount because
there's a useEffect with an empty dependency array calling fetchRecentSearches
and a second useEffect that calls fetchRecentSearches when searchStatus ===
'idle', causing duplicate network calls; consolidate into a single control path
by removing the unconditional mount call and relying on the searchStatus-driven
effect (or vice versa) โ€” update the effects around fetchRecentSearches and
searchStatus so only one effect (either the empty-deps useEffect or the
[searchStatus] useEffect that checks for 'idle') triggers the initial fetch to
eliminate the duplicate call.

---

Duplicate comments:
In `@src/pages/groupSearch/GroupSearch.tsx`:
- Around line 380-387: The handleDelete callback currently calls
deleteRecentSearch asynchronously without catching exceptions; wrap the async
body of handleDelete in a try/catch so any thrown error from deleteRecentSearch
(or subsequent await fetchRecentSearches) is caught and handled (e.g., log via
console/error logger and optionally surface a user-facing error), ensure you
still return/exit early if recentSearches.find(...) returns undefined, and keep
calling fetchRecentSearches only on successful delete responses (res.isSuccess)
inside the try block to avoid unhandled promise rejections.

In `@src/pages/searchBook/SearchBook.tsx`:
- Around line 247-258: The JSX inside the if (error) branch contains a dead
fallback string ("|| '์ฑ… ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.'") because the clause only runs when
error is truthy; update the rendering in SearchBook.tsx to remove the
unreachable fallback or change the condition/render logic so the fallback is
shown when error is falsyโ€”e.g., either remove "|| '์ฑ… ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.'" from the
block that returns Wrapper/Header/IconButton (using symbols error, Wrapper,
Header, IconButton, leftArrow, handleBackButton), or replace the top-level
conditional with a ternary (error ? error : '์ฑ… ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.') so the fallback
can be reached.
- Around line 260-262: The TopBackground is currently rendered with
bookImgUrl={bookDetail?.imageUrl || ''}, causing an empty url("") when
bookDetail is null; change this to conditionally render TopBackground only when
bookDetail?.imageUrl is truthy (use a guard around the TopBackground component)
so you pass a real image URL (bookDetail.imageUrl) or avoid rendering it
entirely when absent; update the JSX near Wrapper/TopBackground references to
use this conditional rendering pattern to prevent empty CSS urls and unnecessary
network/console noise.

---

Nitpick comments:
In `@src/pages/searchBook/SearchBook.tsx`:
- Around line 84-89: The temporary loading timer promise minLoadingTime is
inferred as Promise<unknown>; update its declaration to explicitly type it as
Promise<void> (e.g., const minLoadingTime: Promise<void> = new Promise(resolve
=> setTimeout(resolve, 500))); do the same for the other instance around lines
120-124 so Promise.all([getBookDetail(isbn), getRecruitingRooms(isbn),
minLoadingTime]) keeps correct type inference for getBookDetail and
getRecruitingRooms.

โ„น๏ธ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled

You can enable these sources in your CodeRabbit configuration.

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between 56a615e and 4a62514.

๐Ÿ“’ Files selected for processing (2)
  • src/pages/groupSearch/GroupSearch.tsx
  • src/pages/searchBook/SearchBook.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and canโ€™t be posted inline due to platform limitations.

โš ๏ธ Outside diff range comments (1)
src/pages/groupDetail/GroupDetail.tsx (1)

56-56: โš ๏ธ Potential issue | ๐ŸŸก Minor

isJoining์ด null์ธ ์ฑ„๋กœ ํ•œ ํ”„๋ ˆ์ž„ ๋ Œ๋”๋ง๋˜์–ด ๋ฒ„ํŠผ ํ…์ŠคํŠธ๊ฐ€ ์ˆœ๊ฐ„์ ์œผ๋กœ '์ฐธ์—ฌํ•˜๊ธฐ'๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.

isJoining์€ useEffect๋กœ roomData ๋ณ€ํ™”์— ๋ฐ˜์‘ํ•ด ์„ค์ •๋˜์ง€๋งŒ, React์˜ ํšจ๊ณผ๋Š” ํŽ˜์ธํŠธ ์ดํ›„์— ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ isLoading || !roomData ๊ฐ€๋“œ๊ฐ€ ํ•ด์ œ๋œ ์งํ›„ ํ•œ ํ”„๋ ˆ์ž„ ๋™์•ˆ isJoining === null์ธ ์ƒํƒœ๋กœ ๋ Œ๋”๋ง๋˜์–ด buttonProps๊ฐ€ ์‹ค์ œ๋กœ ์ฐธ์—ฌ ์ค‘์ธ ์‚ฌ์šฉ์ž์—๊ฒŒ๋„ '์ฐธ์—ฌํ•˜๊ธฐ'๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ์ˆœ๊ฐ„์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

isJoining์„ fetch ์™„๋ฃŒ ์‹œ์ ์— ๋ฐ”๋กœ ์„ค์ •ํ•˜๋ฉด ์ด ๋ฌธ์ œ๋ฅผ ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ› ์ œ์•ˆ ์ˆ˜์ •
-  useEffect(() => {
-    if (roomData) {
-      setIsJoining(roomData.isJoining);
-    }
-  }, [roomData]);

fetchRoomDetail ๋‚ด๋ถ€์—์„œ ์ง์ ‘ ์„ค์ •:

        if (response.isSuccess) {
          setRoomData(response.data);
+         setIsJoining(response.data.isJoining);
        } else {

Also applies to: 126-130, 287-292

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/groupDetail/GroupDetail.tsx` at line 56, The transient render
showing '์ฐธ์—ฌํ•˜๊ธฐ' happens because isJoining starts as null and is set in a
useEffect after paint; to fix, initialize isJoining to a non-null default and
set it immediately when room data is fetched: change the useState for isJoining
to a non-null default (e.g., false) and move the logic that determines whether
the current user is joining into fetchRoomDetail so it calls setIsJoining(...)
right after roomData is retrieved; remove or stop relying on the separate
useEffect that sets isJoining based on roomData and ensure buttonProps reads the
updated isJoining set by fetchRoomDetail.
โ™ป๏ธ Duplicate comments (3)
src/pages/searchBook/SearchBook.tsx (2)

247-258: Line 254: error ๊ฐ€๋“œ ๋‚ด๋ถ€ ํด๋ฐฑ ๋ฌธ์ž์—ด์€ dead code์ž…๋‹ˆ๋‹ค.

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/searchBook/SearchBook.tsx` around lines 247 - 258, The current if
(error) block renders {error || '์ฑ… ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.'}, making the fallback string
dead code; change the rendering logic so the fallback displays when error is
falsy. For example, compute a message const message = error || '์ฑ… ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜
์—†์Šต๋‹ˆ๋‹ค.' (or use null-coalescing) and render {message} inside the Wrapper, or
invert the condition to if (!error) show the fallback and if (error) show the
error; update the JSX that uses Wrapper, Header, IconButton, leftArrow and
handleBackButton to render message accordingly.

260-262: TopBackground๊ฐ€ ๋กœ๋”ฉ ์ค‘์— ๋นˆ ๋ฌธ์ž์—ด URL๋กœ ๋ Œ๋”๋ง๋ฉ๋‹ˆ๋‹ค.

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/searchBook/SearchBook.tsx` around lines 260 - 262, TopBackground is
rendered with an empty string while bookDetail is loading; change to render
TopBackground only when a valid image URL exists by wrapping the component in a
conditional that checks bookDetail?.imageUrl (or pass undefined/null instead of
''), e.g., guard the TopBackground render in the return so
TopBackground(bookImgUrl={bookDetail.imageUrl}) only mounts when
bookDetail?.imageUrl is truthy; refer to TopBackground and bookDetail usages in
SearchBook component and the surrounding Wrapper.
src/pages/groupSearch/GroupSearch.tsx (1)

380-387: โš ๏ธ Potential issue | ๐ŸŸก Minor

์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ์‚ญ์ œ ์ฝœ๋ฐฑ์— ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

Line 383์˜ deleteRecentSearch๋Š” ์‹คํŒจ ์‹œ throw ๊ฐ€๋Šฅํ•œ๋ฐ, ํ˜„์žฌ handleDelete์—์„œ try/catch๊ฐ€ ์—†์–ด unhandled rejection์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”ง ์ œ์•ˆ ์ˆ˜์ •์•ˆ
                 handleDelete={async (term: string) => {
                   const x = recentSearches.find(i => i.searchTerm === term);
                   if (!x) return;
-                  const res = await deleteRecentSearch(x.recentSearchId);
-                  if (res.isSuccess) {
-                    await fetchRecentSearches();
+                  try {
+                    const res = await deleteRecentSearch(x.recentSearchId);
+                    if (res.isSuccess) {
+                      await fetchRecentSearches();
+                    }
+                  } catch (error) {
+                    console.error('์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ์‚ญ์ œ ์˜ค๋ฅ˜:', error);
                   }
                 }}
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/groupSearch/GroupSearch.tsx` around lines 380 - 387, The
handleDelete callback risks unhandled rejections because deleteRecentSearch can
throw; wrap the async work in a try/catch inside the handleDelete passed to the
component (the arrow function referencing recentSearches, deleteRecentSearch,
fetchRecentSearches and recentSearchId), call await
deleteRecentSearch(x.recentSearchId) inside the try, on success call await
fetchRecentSearches(), and in the catch log the error and/or surface a
user-facing message (e.g., showToast or processLogger) so failures are handled
gracefully.
๐Ÿงน Nitpick comments (4)
src/pages/searchBook/SearchBook.tsx (1)

120-124: ๋‹จ์ผ ์š”์†Œ Promise.all์€ ๋ถˆํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

getFeedsByIsbn ํ•˜๋‚˜๋งŒ ๊ฐ์‹ธ๋Š” Promise.all๊ณผ ๋ฐฐ์—ด ๊ตฌ์กฐ ๋ถ„ํ•ด const [res]๋Š” ์˜๋ฏธ ์—†๋Š” ๋ณต์žก๋„๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ๋‹จ์ˆœํ•œ await์œผ๋กœ ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๋‹ค.

โ™ป๏ธ ์ œ์•ˆ: ๋‹จ์ˆœ await์œผ๋กœ ๋ณ€๊ฒฝ
-      const [res] = await Promise.all([
-        getFeedsByIsbn(isbn, toFeedSort(selectedFilter), null),
-      ]);
+      const res = await getFeedsByIsbn(isbn, toFeedSort(selectedFilter), null);
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/searchBook/SearchBook.tsx` around lines 120 - 124, The code uses
Promise.all with a single element and array destructuring (const [res] = await
Promise.all([ getFeedsByIsbn(isbn, toFeedSort(selectedFilter), null) ])), which
is unnecessary; replace it with a direct await call to getFeedsByIsbn (e.g.,
const res = await getFeedsByIsbn(isbn, toFeedSort(selectedFilter), null)) and
keep the existing minLoadingTime await, ensuring references to minLoadingTime,
getFeedsByIsbn, toFeedSort, selectedFilter, and isbn are preserved and no array
destructuring is used.
src/pages/groupSearch/GroupSearch.tsx (1)

93-105: ์ตœ์†Œ ์ง€์—ฐ ๋Œ€๊ธฐ ๋กœ์ง์„ Promise.all ํ•œ ๋ฒˆ์œผ๋กœ ํ†ต์ผํ•ด๋„ ์ข‹๊ฒ ์Šต๋‹ˆ๋‹ค.

ํ˜„์žฌ ๋™์ž‘์€ ๋งž์ง€๋งŒ(await minLoadingTime ๋ณ„๋„ ๋Œ€๊ธฐ), fetchRecentSearches์™€ ํŒจํ„ด์„ ํ†ต์ผํ•˜๋ฉด ์˜๋„ ํŒŒ์•…์ด ๋” ์‰ฌ์›Œ์ง‘๋‹ˆ๋‹ค.

๐Ÿ”ง ์ œ์•ˆ ์ˆ˜์ •์•ˆ
       try {
         const isFinalized = status === 'searched';
         const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500));
-        const [res] = await Promise.all([
+        const [res] = await Promise.all([
           getSearchRooms(
             term.trim(),
             sortKey,
             undefined,
             isFinalized,
             categoryParam,
             isAllCategory,
           ),
+          minLoadingTime,
         ]);
-        await minLoadingTime;
         if (res.isSuccess) {
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/groupSearch/GroupSearch.tsx` around lines 93 - 105, Unify the
minimum loading delay with the fetch by including the minLoadingTime promise in
the same Promise.all call instead of awaiting it separately: replace the current
separate minLoadingTime creation + awaiting with Promise.all([
getSearchRooms(...), minLoadingTime ]) and destructure the getSearchRooms result
(e.g., const [res] = await Promise.all([...]) or const [res] = ... depending on
order) so the logic in getSearchRooms, minLoadingTime, and the subsequent if
(res.isSuccess) remains unchanged (mirror the fetchRecentSearches pattern).
src/pages/groupDetail/ParticipatedGroupDetail.tsx (1)

82-86: Promise.all ๋ฐฐ์—ด์— minLoadingTime์„ ํฌํ•จํ•˜์„ธ์š”.

ํ˜„์žฌ ์ฝ”๋“œ์—์„œ Promise.all์€ getRoomPlaying ํ•˜๋‚˜๋งŒ ํฌํ•จํ•˜๊ณ , minLoadingTime์€ ๋ณ„๋„๋กœ awaitํ•ฉ๋‹ˆ๋‹ค. ๋™์ž‘์€ ๋™์ผํ•˜์ง€๋งŒ, Promise.all์˜ ์˜๋„๊ฐ€ ๋ถˆ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค. minLoadingTime์„ ๋ฐฐ์—ด ์•ˆ์— ํ•จ๊ป˜ ๋„ฃ๋Š” ๊ฒƒ์ด ๋ณ‘๋ ฌ ์‹คํ–‰ ํŒจํ„ด์˜ ๊ด€์šฉ์ ์ธ ํ‘œํ˜„์ž…๋‹ˆ๋‹ค.

โ™ป๏ธ ์ œ์•ˆ ์ˆ˜์ •
-        const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500));
-        const [response] = await Promise.all([
-          getRoomPlaying(parseInt(roomId)),
-        ]);
-        await minLoadingTime;
+        const [response] = await Promise.all([
+          getRoomPlaying(parseInt(roomId)),
+          new Promise(resolve => setTimeout(resolve, 500)),
+        ]);
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/groupDetail/ParticipatedGroupDetail.tsx` around lines 82 - 86, The
current code creates minLoadingTime and awaits it separately after awaiting
Promise.all([getRoomPlaying(parseInt(roomId))]); change this to run both in
parallel by moving minLoadingTime into the Promise.all call so Promise.all([
getRoomPlaying(parseInt(roomId)), minLoadingTime ]) is awaited together (and
remove the separate await minLoadingTime); this keeps the existing response
extraction from the array returned by Promise.all while ensuring the artificial
minimum loading delay runs in parallel with getRoomPlaying.
src/pages/groupDetail/GroupDetail.tsx (1)

93-97: Promise.all์— ๋‹จ์ผ ์š”์†Œ๋งŒ ํฌํ•จ๋˜์–ด minLoadingTime์ด ๋ฐฐ์—ด ๋ฐ–์—์„œ ๋”ฐ๋กœ await๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

minLoadingTime์€ ํƒ€์ด๋จธ ์‹œ์ž‘ ํ›„ Promise.all๊ณผ ๋ณ„๊ฐœ๋กœ await๋˜๋ฏ€๋กœ ๋™์ž‘์€ ์˜ฌ๋ฐ”๋ฅด์ง€๋งŒ, Promise.all์ด ๋‹จ์ผ ์›์†Œ๋งŒ ๊ฐ์‹ธ๊ณ  ์žˆ์–ด ์˜๋„๊ฐ€ ๋ชจํ˜ธํ•ฉ๋‹ˆ๋‹ค. minLoadingTime์„ ๋ฐฐ์—ด ์•ˆ์— ํฌํ•จ์‹œํ‚ค๋Š” ๊ฒƒ์ด ๊ด€์šฉ์ ์ด๋ฉฐ ๊ฐ€๋…์„ฑ๋„ ๋†’์Šต๋‹ˆ๋‹ค.

โ™ป๏ธ ์ œ์•ˆ ๋ฆฌํŒฉํ„ฐ
-        const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500));
-        const [response] = await Promise.all([
-          getRoomDetail(Number(roomId)),
-        ]);
-        await minLoadingTime;
+        const [response] = await Promise.all([
+          getRoomDetail(Number(roomId)),
+          new Promise(resolve => setTimeout(resolve, 500)),
+        ]);
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/groupDetail/GroupDetail.tsx` around lines 93 - 97, The code
currently awaits minLoadingTime separately, which is confusing because
Promise.all only contains getRoomDetail; update the await to include
minLoadingTime inside the Promise.all call so both promises are awaited together
(e.g., call Promise.all([ getRoomDetail(Number(roomId)), minLoadingTime ]) and
adjust destructuring to capture getRoomDetail's result (keep const [response] =
...) and remove the separate await minLoadingTime; reference: minLoadingTime,
getRoomDetail, roomId, response.
๐Ÿค– Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/search/MostSearchedBooks.tsx`:
- Around line 30-35: The current fail-fast Promise.all can skip awaiting
minLoadingTime when getMostSearchedBooks() rejects; change the logic so
minLoadingTime is always awaited before you update loading state or render
error: start minLoadingTime as now, run the fetch using Promise.allSettled (or
wrap getMostSearchedBooks() into a promise that never rejects), then await
minLoadingTime before handling results; in practice replace Promise.all([...])
with Promise.allSettled([getMostSearchedBooks()]) (referencing
getMostSearchedBooks, minLoadingTime and setIsLoading in MostSearchedBooks.tsx),
then after await minLoadingTime inspect the settled result (response.status ===
'fulfilled' ? response.value : handle error) and only then call
setIsLoading(false) or set error state so the 500ms skeleton is always honored.

In `@src/pages/groupDetail/ParticipatedGroupDetail.tsx`:
- Around line 67-69: The back-navigation behavior is inconsistent:
handleBackClick uses navigate(-1) while handleBackButton uses navigate('/group')
and their names are confusing; pick the intended behavior (either always go to
'/group' or always go back one step), replace the other handler so both error
and normal/loading states call the same function (e.g., unify into a single
handler name like handleBack or handleNavigateBack), update all onLeftClick
usages (including the error-state onLeftClick) to use that unified handler, and
remove the redundant handler to avoid duplicate implementations.

---

Outside diff comments:
In `@src/pages/groupDetail/GroupDetail.tsx`:
- Line 56: The transient render showing '์ฐธ์—ฌํ•˜๊ธฐ' happens because isJoining starts
as null and is set in a useEffect after paint; to fix, initialize isJoining to a
non-null default and set it immediately when room data is fetched: change the
useState for isJoining to a non-null default (e.g., false) and move the logic
that determines whether the current user is joining into fetchRoomDetail so it
calls setIsJoining(...) right after roomData is retrieved; remove or stop
relying on the separate useEffect that sets isJoining based on roomData and
ensure buttonProps reads the updated isJoining set by fetchRoomDetail.

---

Duplicate comments:
In `@src/pages/groupSearch/GroupSearch.tsx`:
- Around line 380-387: The handleDelete callback risks unhandled rejections
because deleteRecentSearch can throw; wrap the async work in a try/catch inside
the handleDelete passed to the component (the arrow function referencing
recentSearches, deleteRecentSearch, fetchRecentSearches and recentSearchId),
call await deleteRecentSearch(x.recentSearchId) inside the try, on success call
await fetchRecentSearches(), and in the catch log the error and/or surface a
user-facing message (e.g., showToast or processLogger) so failures are handled
gracefully.

In `@src/pages/searchBook/SearchBook.tsx`:
- Around line 247-258: The current if (error) block renders {error || '์ฑ… ์ •๋ณด๋ฅผ ์ฐพ์„
์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.'}, making the fallback string dead code; change the rendering logic so
the fallback displays when error is falsy. For example, compute a message const
message = error || '์ฑ… ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.' (or use null-coalescing) and render
{message} inside the Wrapper, or invert the condition to if (!error) show the
fallback and if (error) show the error; update the JSX that uses Wrapper,
Header, IconButton, leftArrow and handleBackButton to render message
accordingly.
- Around line 260-262: TopBackground is rendered with an empty string while
bookDetail is loading; change to render TopBackground only when a valid image
URL exists by wrapping the component in a conditional that checks
bookDetail?.imageUrl (or pass undefined/null instead of ''), e.g., guard the
TopBackground render in the return so
TopBackground(bookImgUrl={bookDetail.imageUrl}) only mounts when
bookDetail?.imageUrl is truthy; refer to TopBackground and bookDetail usages in
SearchBook component and the surrounding Wrapper.

---

Nitpick comments:
In `@src/pages/groupDetail/GroupDetail.tsx`:
- Around line 93-97: The code currently awaits minLoadingTime separately, which
is confusing because Promise.all only contains getRoomDetail; update the await
to include minLoadingTime inside the Promise.all call so both promises are
awaited together (e.g., call Promise.all([ getRoomDetail(Number(roomId)),
minLoadingTime ]) and adjust destructuring to capture getRoomDetail's result
(keep const [response] = ...) and remove the separate await minLoadingTime;
reference: minLoadingTime, getRoomDetail, roomId, response.

In `@src/pages/groupDetail/ParticipatedGroupDetail.tsx`:
- Around line 82-86: The current code creates minLoadingTime and awaits it
separately after awaiting Promise.all([getRoomPlaying(parseInt(roomId))]);
change this to run both in parallel by moving minLoadingTime into the
Promise.all call so Promise.all([ getRoomPlaying(parseInt(roomId)),
minLoadingTime ]) is awaited together (and remove the separate await
minLoadingTime); this keeps the existing response extraction from the array
returned by Promise.all while ensuring the artificial minimum loading delay runs
in parallel with getRoomPlaying.

In `@src/pages/groupSearch/GroupSearch.tsx`:
- Around line 93-105: Unify the minimum loading delay with the fetch by
including the minLoadingTime promise in the same Promise.all call instead of
awaiting it separately: replace the current separate minLoadingTime creation +
awaiting with Promise.all([ getSearchRooms(...), minLoadingTime ]) and
destructure the getSearchRooms result (e.g., const [res] = await
Promise.all([...]) or const [res] = ... depending on order) so the logic in
getSearchRooms, minLoadingTime, and the subsequent if (res.isSuccess) remains
unchanged (mirror the fetchRecentSearches pattern).

In `@src/pages/searchBook/SearchBook.tsx`:
- Around line 120-124: The code uses Promise.all with a single element and array
destructuring (const [res] = await Promise.all([ getFeedsByIsbn(isbn,
toFeedSort(selectedFilter), null) ])), which is unnecessary; replace it with a
direct await call to getFeedsByIsbn (e.g., const res = await
getFeedsByIsbn(isbn, toFeedSort(selectedFilter), null)) and keep the existing
minLoadingTime await, ensuring references to minLoadingTime, getFeedsByIsbn,
toFeedSort, selectedFilter, and isbn are preserved and no array destructuring is
used.

โ„น๏ธ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled

You can enable these sources in your CodeRabbit configuration.

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between 4a62514 and 9239963.

๐Ÿ“’ Files selected for processing (13)
  • src/components/feed/MyFeed.tsx
  • src/components/search/MostSearchedBooks.tsx
  • src/pages/feed/FeedDetailPage.tsx
  • src/pages/feed/FollowerListPage.tsx
  • src/pages/feed/MyFeedPage.tsx
  • src/pages/feed/OtherFeedPage.tsx
  • src/pages/groupDetail/GroupDetail.tsx
  • src/pages/groupDetail/ParticipatedGroupDetail.tsx
  • src/pages/groupMembers/GroupMembers.tsx
  • src/pages/groupSearch/GroupSearch.tsx
  • src/pages/mypage/SavePage.tsx
  • src/pages/search/Search.tsx
  • src/pages/searchBook/SearchBook.tsx
๐Ÿšง Files skipped from review as they are similar to previous changes (6)
  • src/pages/feed/FeedDetailPage.tsx
  • src/pages/mypage/SavePage.tsx
  • src/pages/groupMembers/GroupMembers.tsx
  • src/pages/search/Search.tsx
  • src/pages/feed/MyFeedPage.tsx
  • src/pages/feed/FollowerListPage.tsx

Comment on lines +30 to 35
const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500));
const [response] = await Promise.all([
getMostSearchedBooks(),
]);
await minLoadingTime;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸ  Major

๐Ÿงฉ Analysis chain

๐ŸŒ Web query:

Promise.all reject immediately vs Promise.allSettled wait for all promises

๐Ÿ’ก Result:

  • Promise.all(iterable): returns a promise that fulfills only if all input promises fulfill, but rejects as soon as the first one rejects (fail-fast). The rejection reason is from that first rejection. (mdn2.netlify.app)

    • Important: it does not cancel the other promisesโ€”those async operations may keep running; you just stop waiting for their results via that Promise.all call. (javascript.info)
  • Promise.allSettled(iterable): returns a promise that waits until every input promise settles (fulfilled or rejected), then fulfills with an array like [{status:"fulfilled", value}, {status:"rejected", reason}, ...] in the original order. (developer.mozilla.org)

Citations:


๐Ÿ Script executed:

cd /repo && cat -n src/components/search/MostSearchedBooks.tsx | head -80

Repository: THIP-TextHip/THIP-Web

Length of output: 120


๐Ÿ Script executed:

git ls-files | grep -i "MostSearchedBooks.tsx"

Repository: THIP-TextHip/THIP-Web

Length of output: 108


๐Ÿ Script executed:

git ls-files | grep -E "search|components" | head -20

Repository: THIP-TextHip/THIP-Web

Length of output: 914


๐Ÿ Script executed:

cat -n src/components/search/MostSearchedBooks.tsx

Repository: THIP-TextHip/THIP-Web

Length of output: 3329


๐Ÿ Script executed:

git ls-files | grep -i "getMostSearchedBooks"

Repository: THIP-TextHip/THIP-Web

Length of output: 102


๐Ÿ Script executed:

cat -n src/api/books/getMostSearchedBooks.ts 2>/dev/null || find . -name "*getMostSearchedBooks*" -type f

Repository: THIP-TextHip/THIP-Web

Length of output: 932


์‹คํŒจ ๊ฒฝ๋กœ์—์„œ 500ms ์ตœ์†Œ ๋กœ๋”ฉ ์‹œ๊ฐ„์ด ๋ณด์žฅ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

Promise.all์€ ์ฒซ ๋ฒˆ์งธ Promise ๊ฑฐ๋ถ€ ์‹œ ์ฆ‰์‹œ ์‹คํŒจํ•˜๋ฏ€๋กœ(fail-fast), getMostSearchedBooks()์ด ์—๋Ÿฌ๋ฅผ throwํ•˜๋ฉด Line 34์˜ await minLoadingTime์— ๋„๋‹ฌํ•˜์ง€ ์•Š๊ณ  catch ๋ธ”๋ก์œผ๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค. ๊ฒฐ๊ณผ์ ์œผ๋กœ setIsLoading(false)๊ฐ€ ์ง€์—ฐ ์—†์ด ์‹คํ–‰๋˜์–ด ์Šค์ผˆ๋ ˆํ†ค์ด 500ms ๋ฏธ๋งŒ์œผ๋กœ ํ‘œ์‹œ๋˜๊ณ , ์ฆ‰์‹œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋กœ ์ „ํ™˜๋˜์–ด ํ™”๋ฉด ๊นœ๋นก์ž„์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”ง ์ œ์•ˆ ์ˆ˜์ •์•ˆ
-        const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500));
-        const [response] = await Promise.all([
-          getMostSearchedBooks(),
-        ]);
-        await minLoadingTime;
+        const minLoadingTime = new Promise<void>(resolve => setTimeout(resolve, 500));
+        const [responseResult] = await Promise.allSettled([getMostSearchedBooks()]);
+        await minLoadingTime;
+
+        if (responseResult.status === 'rejected') {
+          throw responseResult.reason;
+        }
+        const response = responseResult.value;
๐Ÿ“ 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 minLoadingTime = new Promise(resolve => setTimeout(resolve, 500));
const [response] = await Promise.all([
getMostSearchedBooks(),
]);
await minLoadingTime;
const minLoadingTime = new Promise<void>(resolve => setTimeout(resolve, 500));
const [responseResult] = await Promise.allSettled([getMostSearchedBooks()]);
await minLoadingTime;
if (responseResult.status === 'rejected') {
throw responseResult.reason;
}
const response = responseResult.value;
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/search/MostSearchedBooks.tsx` around lines 30 - 35, The
current fail-fast Promise.all can skip awaiting minLoadingTime when
getMostSearchedBooks() rejects; change the logic so minLoadingTime is always
awaited before you update loading state or render error: start minLoadingTime as
now, run the fetch using Promise.allSettled (or wrap getMostSearchedBooks() into
a promise that never rejects), then await minLoadingTime before handling
results; in practice replace Promise.all([...]) with
Promise.allSettled([getMostSearchedBooks()]) (referencing getMostSearchedBooks,
minLoadingTime and setIsLoading in MostSearchedBooks.tsx), then after await
minLoadingTime inspect the settled result (response.status === 'fulfilled' ?
response.value : handle error) and only then call setIsLoading(false) or set
error state so the 500ms skeleton is always honored.

Comment on lines +67 to +69
const handleBackClick = () => {
navigate(-1);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸก Minor

์—๋Ÿฌ/๋กœ๋”ฉ ์ƒํƒœ์˜ ๋’ค๋กœ๊ฐ€๊ธฐ ๋™์ž‘ ๋ถˆ์ผ์น˜๋ฅผ ํ™•์ธํ•˜์„ธ์š”.

handleBackClick(์—๋Ÿฌ ์ƒํƒœ)์€ navigate(-1)์„ ์‚ฌ์šฉํ•˜๊ณ , handleBackButton(๋กœ๋”ฉยท์ •์ƒ ์ƒํƒœ)์€ navigate('/group')๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ™์€ ๋’ค๋กœ๊ฐ€๊ธฐ ๋ฒ„ํŠผ์ด ์ƒํƒœ์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ๋ชฉ์ ์ง€๋กœ ์ด๋™ํ•˜๋ฉด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ์ผ๊ด€๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • ์—๋Ÿฌ ์ƒํƒœ์—์„œ๋„ /group์œผ๋กœ ์ด๋™ํ•˜๋Š” ๊ฒƒ์ด ์˜๋„๋ผ๋ฉด handleBackClick์„ handleBackButton์œผ๋กœ ๋Œ€์ฒดํ•˜๊ณ  ์ค‘๋ณต ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.
  • navigate(-1)์ด ์˜๋„์ ์ด๋ผ๋ฉด ๋กœ๋”ฉ ์ƒํƒœ์˜ ๋’ค๋กœ๊ฐ€๊ธฐ๋„ ๋™์ผํ•˜๊ฒŒ ๋งž์ถ”๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

์ถ”๊ฐ€๋กœ, ๋‘ ํ•ธ๋“ค๋Ÿฌ์˜ ์ด๋ฆ„(handleBackClick vs handleBackButton)์ด ์˜๋ฏธ์ƒ ๊ตฌ๋ถ„์ด ๋ถˆ๋ช…ํ™•ํ•˜์—ฌ ํ˜ผ๋ž€์„ ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

โ™ป๏ธ ๋‹จ์ผ ํ•ธ๋“ค๋Ÿฌ๋กœ ํ†ต์ผํ•˜๋Š” ๊ฒฝ์šฐ์˜ ์˜ˆ์‹œ
-  const handleBackClick = () => {
-    navigate(-1);
-  };
-
   ...

   const handleBackButton = () => {
     navigate('/group');
   };

๊ทธ๋ฆฌ๊ณ  ์—๋Ÿฌ ์ƒํƒœ์˜ onLeftClick์„ handleBackButton์œผ๋กœ ๊ต์ฒด:

-          onLeftClick={handleBackClick}
+          onLeftClick={handleBackButton}

Also applies to: 110-112

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/groupDetail/ParticipatedGroupDetail.tsx` around lines 67 - 69, The
back-navigation behavior is inconsistent: handleBackClick uses navigate(-1)
while handleBackButton uses navigate('/group') and their names are confusing;
pick the intended behavior (either always go to '/group' or always go back one
step), replace the other handler so both error and normal/loading states call
the same function (e.g., unify into a single handler name like handleBack or
handleNavigateBack), update all onLeftClick usages (including the error-state
onLeftClick) to use that unified handler, and remove the redundant handler to
avoid duplicate implementations.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and canโ€™t be posted inline due to platform limitations.

โš ๏ธ Outside diff range comments (2)
src/pages/memory/Memory.tsx (1)

127-161: โš ๏ธ Potential issue | ๐ŸŸก Minor

์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ 500ms ์ตœ์†Œ ๋กœ๋”ฉ ์‹œ๊ฐ„์ด ๋ณด์žฅ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

Promise.all์€ ํ•˜๋‚˜์˜ Promise๊ฐ€ reject๋˜๋ฉด ์ฆ‰์‹œ reject๋ฉ๋‹ˆ๋‹ค. getMemoryPosts๊ฐ€ 100ms ๋งŒ์— ์‹คํŒจํ•˜๋ฉด minLoadingTime (500ms)์„ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š๊ณ  ๋ฐ”๋กœ catch๋กœ ์ง„์ž…ํ•˜์—ฌ ์Šค์ผˆ๋ ˆํ†ค์ด ์งง๊ฒŒ ๊นœ๋นก์ด๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

์ตœ์†Œ ๋กœ๋”ฉ ์‹œ๊ฐ„์„ ์—๋Ÿฌ ๊ฒฝ๋กœ์—์„œ๋„ ๋ณด์žฅํ•˜๋ ค๋ฉด fetch์™€ delay๋ฅผ ๋ถ„๋ฆฌํ•ด์„œ ์ฒ˜๋ฆฌํ•˜์„ธ์š”.

๐Ÿ› ๏ธ ์ˆ˜์ • ์ œ์•ˆ
-      const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500));
-      const [response] = await Promise.all([getMemoryPosts(params), minLoadingTime]);
-
+      const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500));
+      const [result] = await Promise.allSettled([getMemoryPosts(params), minLoadingTime]);
+      if (result.status === 'rejected') throw result.reason;
+      const response = result.value;

๋˜๋Š” ๋” ๊ฐ„๋‹จํ•˜๊ฒŒ:

-      const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500));
-      const [response] = await Promise.all([getMemoryPosts(params), minLoadingTime]);
+      const startTime = Date.now();
+      const response = await getMemoryPosts(params);
+      const elapsed = Date.now() - startTime;
+      if (elapsed < 500) await new Promise(resolve => setTimeout(resolve, 500 - elapsed));

๋‘ ๋ฒˆ์งธ ๋ฐฉ์‹์€ ์—๋Ÿฌ ์‹œ์—๋„ catch ์ „์— ์ตœ์†Œ ์‹œ๊ฐ„์„ ๋ณด์žฅํ•˜๋ ค๋ฉด finally ๋ธ”๋ก์—์„œ delay๋ฅผ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•˜๋ฏ€๋กœ, Promise.allSettled ๋ฐฉ์‹์ด ๋” ๊น”๋”ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/memory/Memory.tsx` around lines 127 - 161, The current fetch uses
Promise.all([getMemoryPosts(params), minLoadingTime]) so a fast rejection from
getMemoryPosts skips the 500ms delay; change the logic to await the fetch and
the delay separately (e.g., use Promise.allSettled([getMemoryPosts(params),
minLoadingTime]) or await getMemoryPosts(params) and then await minLoadingTime
in a finally) so that minLoadingTime is always honored even on error; adjust
handling around getMemoryPosts, minLoadingTime, and the existing
try/catch/finally blocks so setLoading(false) and UI updates occur after the
ensured delay.
src/pages/groupSearch/GroupSearch.tsx (1)

50-59: โš ๏ธ Potential issue | ๐ŸŸก Minor

์ดˆ๊ธฐ ๋งˆ์šดํŠธ ์‹œ ์ตœ๊ทผ ๊ฒ€์ƒ‰ ์š”์ฒญ์ด ์ค‘๋ณต ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.

Line 51์˜ ๋งˆ์šดํŠธ ์ „์šฉ effect์™€ Line 57์˜ searchStatus === 'idle' effect๊ฐ€ ์ฒซ ๋ Œ๋”์—์„œ ๋ชจ๋‘ ์‹คํ–‰๋˜์–ด ๋™์ผ API๋ฅผ 2ํšŒ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”ง ์ œ์•ˆ ์ˆ˜์ •์•ˆ
-  useEffect(() => {
-    fetchRecentSearches();
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, []);
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/groupSearch/GroupSearch.tsx` around lines 50 - 59, The component
calls fetchRecentSearches twice on first render because there's a mount-only
useEffect and another useEffect that calls fetchRecentSearches when searchStatus
=== 'idle'; remove the duplicate by either deleting the mount-only effect or
preventing the second effect from running on initial mount (e.g., add a
hasFetchedRef or guard so the useEffect watching searchStatus only calls
fetchRecentSearches when searchStatus becomes 'idle' after mount). Update code
references: fetchRecentSearches, searchStatus, and the two useEffect blocks so
only one path triggers the API call on initial render.
โ™ป๏ธ Duplicate comments (2)
src/pages/mypage/Mypage.tsx (1)

108-119: โš ๏ธ Potential issue | ๐ŸŸก Minor

์Šค์ผˆ๋ ˆํ†ค ๋ฌด๊ธฐํ•œ ๋…ธ์ถœ ๋ฒ„๊ทธ ์ˆ˜์ • ํ™•์ธ.

loading๊ณผ !profile์„ ๋ถ„๋ฆฌํ•˜์—ฌ ํ”„๋กœํ•„ ๋กœ๋“œ ์‹คํŒจ ์‹œ์—๋„ ์Šค์ผˆ๋ ˆํ†ค์ด ์‚ฌ๋ผ์ง€๊ฒŒ ๋œ ์ ์€ ์˜ฌ๋ฐ”๋ฅธ ์ˆ˜์ •์ž…๋‹ˆ๋‹ค.

๋‹ค๋งŒ, !profile ? null ๋ถ„๊ธฐ์—์„œ ์‚ฌ์šฉ์ž์—๊ฒŒ ์•„๋ฌด๋Ÿฐ ํ”ผ๋“œ๋ฐฑ์ด ์ œ๊ณต๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ”„๋กœํ•„ ๋กœ๋“œ ์‹คํŒจ ์‹œ ๋นˆ ํ™”๋ฉด ๋Œ€์‹  ๊ฐ„๋‹จํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ํ‘œ์‹œํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ’ก fallback UI ์ œ์•ˆ
-        ) : !profile ? null : (
+        ) : !profile ? (
+          <UserProfile>
+            <div className="userInfo">
+              <div className="user">
+                <div className="username">ํ”„๋กœํ•„์„ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์–ด์š”</div>
+              </div>
+            </div>
+          </UserProfile>
+        ) : (
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/mypage/Mypage.tsx` around lines 108 - 119, The current conditional
rendering removes the skeleton when loading is false but returns null on the
!profile branch, leaving users with no feedback on load failure; update the
conditional in Mypage.tsx so that when loading is false and profile is falsy you
render a simple fallback/error UI (e.g., a ProfileErrorContainer or plain
message like "Failed to load profile. Retry.") instead of null; keep the loading
branch using ProfileSkeletonContainer/ProfileSkeletonLeft/ProfileSkeletonText
and ensure the new fallback replaces the !profile ? null branch so users see an
error state and can act.
src/pages/memory/Memory.tsx (1)

318-342: inert ์†์„ฑ ์ ์šฉ โ€” ์ด์ „ ๋ฆฌ๋ทฐ ํ”ผ๋“œ๋ฐฑ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋ฐ˜์˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

FixedSection์— inert ์†์„ฑ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ๋กœ๋”ฉ ์ค‘ ํ‚ค๋ณด๋“œ/ํฌ์ปค์Šค/ํด๋ฆญ ์ƒํ˜ธ์ž‘์šฉ์„ ์™„์ „ํžˆ ์ฐจ๋‹จํ•œ ๊ฒƒ์€ ์ด์ „ ๋ฆฌ๋ทฐ์—์„œ ์ œ์•ˆ๋œ ๋‚ด์šฉ๊ณผ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/memory/Memory.tsx` around lines 318 - 342, The "inert" application
on FixedSection during loading is correct, but the diffs include an extraneous
review marker [duplicate_comment]; remove that stray marker and ensure the JSX
uses the inert prop on FixedSection only while loading (keep FixedSection inert
as shown โ€” inert or inert={true} is acceptable) so that keyboard/focus/click
interactions remain blocked; verify no other instances of the duplicate marker
remain.
๐Ÿงน Nitpick comments (1)
src/pages/memory/Memory.tsx (1)

91-91: ํƒญ/ํ•„ํ„ฐ ๋ณ€๊ฒฝ ์‹œ์—๋„ ์ „์ฒด ์Šค์ผˆ๋ ˆํ†ค์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค โ€” ์˜๋„๋œ ๋™์ž‘์ธ์ง€ ํ™•์ธํ•˜์„ธ์š”.

loading์ด true๋กœ ์ดˆ๊ธฐํ™”๋˜๊ณ , loadMemoryPosts ํ˜ธ์ถœ ์‹œ๋งˆ๋‹ค setLoading(true)๋กœ ์„ค์ •๋˜๋ฏ€๋กœ ํƒญ ์ „ํ™˜์ด๋‚˜ ํ•„ํ„ฐ ๋ณ€๊ฒฝ ์‹œ์—๋„ ์ „์ฒด ์ฝ˜ํ…์ธ ๊ฐ€ ์Šค์ผˆ๋ ˆํ†ค์œผ๋กœ ๋Œ€์ฒด๋ฉ๋‹ˆ๋‹ค. ์ด๋ฏธ ๋ฐ์ดํ„ฐ๊ฐ€ ํ‘œ์‹œ๋œ ์ƒํƒœ์—์„œ์˜ ์ „ํ™˜์ด๋ผ๋ฉด ์ฝ˜ํ…์ธ  ๊นœ๋นก์ž„์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜๋„๋œ ๋””์ž์ธ์ด๋ผ๋ฉด ๋ฌด์‹œํ•˜์…”๋„ ๋ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ ์•„๋‹ˆ๋ผ๋ฉด, ์ดˆ๊ธฐ ๋กœ๋”ฉ๊ณผ ์ดํ›„ ์žฌ๋กœ๋”ฉ์„ ๊ตฌ๋ถ„ํ•˜์—ฌ(isInitialLoad ๋“ฑ) ์žฌ๋กœ๋”ฉ ์‹œ์—๋Š” ๊ธฐ์กด ์ฝ˜ํ…์ธ  ์œ„์— ๊ฐ€๋ฒผ์šด ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ๋ฐฉ์‹๋„ ๊ณ ๋ คํ•ด ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Also applies to: 107-107

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/memory/Memory.tsx` at line 91, The current behavior sets loading =
true on every loadMemoryPosts call (useState( true ) + setLoading(true)),
causing full skeleton replacement on tab/filter changes; update the logic to
distinguish initial load vs subsequent reloads (e.g., add an isInitialLoad state
or useRef) and only set loading=true for the initial fetch in the Memory
component, while for subsequent calls show a lighter inline spinner or set a
separate isRefetching flag so existing content remains visible during tab/filter
changes; adjust references to loading, setLoading and loadMemoryPosts
accordingly to use the new initial/refetch flags.
๐Ÿค– Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/pages/groupSearch/GroupSearch.tsx`:
- Around line 64-66: The current Promise.all usage with minLoadingTime allows
early rejection and skips the 500ms delay on failures; change both places to use
Promise.allSettled (or otherwise ensure minLoadingTime is always awaited). For
the getRecentSearch flow, replace Promise.all([getRecentSearch('ROOM'),
minLoadingTime]) with Promise.allSettled([getRecentSearch('ROOM'),
minLoadingTime]) and then check the settled result for the getRecentSearch entry
(status === 'fulfilled' and value.isSuccess) before calling setRecentSearches,
defaulting to [] on failure; remove the later separate await minLoadingTime if
present. Do the same for the getSearchRooms call: include minLoadingTime in
Promise.allSettled([getSearchRooms(...), minLoadingTime]) and extract the rooms
only if the getSearchRooms settled entry succeeded, otherwise handle the error
path after the delay so success and failure both respect the 500ms minimum.

---

Outside diff comments:
In `@src/pages/groupSearch/GroupSearch.tsx`:
- Around line 50-59: The component calls fetchRecentSearches twice on first
render because there's a mount-only useEffect and another useEffect that calls
fetchRecentSearches when searchStatus === 'idle'; remove the duplicate by either
deleting the mount-only effect or preventing the second effect from running on
initial mount (e.g., add a hasFetchedRef or guard so the useEffect watching
searchStatus only calls fetchRecentSearches when searchStatus becomes 'idle'
after mount). Update code references: fetchRecentSearches, searchStatus, and the
two useEffect blocks so only one path triggers the API call on initial render.

In `@src/pages/memory/Memory.tsx`:
- Around line 127-161: The current fetch uses
Promise.all([getMemoryPosts(params), minLoadingTime]) so a fast rejection from
getMemoryPosts skips the 500ms delay; change the logic to await the fetch and
the delay separately (e.g., use Promise.allSettled([getMemoryPosts(params),
minLoadingTime]) or await getMemoryPosts(params) and then await minLoadingTime
in a finally) so that minLoadingTime is always honored even on error; adjust
handling around getMemoryPosts, minLoadingTime, and the existing
try/catch/finally blocks so setLoading(false) and UI updates occur after the
ensured delay.

---

Duplicate comments:
In `@src/pages/memory/Memory.tsx`:
- Around line 318-342: The "inert" application on FixedSection during loading is
correct, but the diffs include an extraneous review marker [duplicate_comment];
remove that stray marker and ensure the JSX uses the inert prop on FixedSection
only while loading (keep FixedSection inert as shown โ€” inert or inert={true} is
acceptable) so that keyboard/focus/click interactions remain blocked; verify no
other instances of the duplicate marker remain.

In `@src/pages/mypage/Mypage.tsx`:
- Around line 108-119: The current conditional rendering removes the skeleton
when loading is false but returns null on the !profile branch, leaving users
with no feedback on load failure; update the conditional in Mypage.tsx so that
when loading is false and profile is falsy you render a simple fallback/error UI
(e.g., a ProfileErrorContainer or plain message like "Failed to load profile.
Retry.") instead of null; keep the loading branch using
ProfileSkeletonContainer/ProfileSkeletonLeft/ProfileSkeletonText and ensure the
new fallback replaces the !profile ? null branch so users see an error state and
can act.

---

Nitpick comments:
In `@src/pages/memory/Memory.tsx`:
- Line 91: The current behavior sets loading = true on every loadMemoryPosts
call (useState( true ) + setLoading(true)), causing full skeleton replacement on
tab/filter changes; update the logic to distinguish initial load vs subsequent
reloads (e.g., add an isInitialLoad state or useRef) and only set loading=true
for the initial fetch in the Memory component, while for subsequent calls show a
lighter inline spinner or set a separate isRefetching flag so existing content
remains visible during tab/filter changes; adjust references to loading,
setLoading and loadMemoryPosts accordingly to use the new initial/refetch flags.

โ„น๏ธ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled

You can enable these sources in your CodeRabbit configuration.

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between 9239963 and f38029c.

๐Ÿ“’ Files selected for processing (3)
  • src/pages/groupSearch/GroupSearch.tsx
  • src/pages/memory/Memory.tsx
  • src/pages/mypage/Mypage.tsx

Comment on lines +64 to 66
const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500));
const [response] = await Promise.all([getRecentSearch('ROOM'), minLoadingTime]);
setRecentSearches(response.isSuccess ? response.data.recentSearchList : []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸ  Major

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

fd 'GroupSearch.tsx' --type f

Repository: THIP-TextHip/THIP-Web

Length of output: 102


๐Ÿ Script executed:

cat -n src/pages/groupSearch/GroupSearch.tsx | sed -n '50,110p'

Repository: THIP-TextHip/THIP-Web

Length of output: 2189


๐Ÿ Script executed:

cat -n src/pages/groupSearch/GroupSearch.tsx | sed -n '105,125p'

Repository: THIP-TextHip/THIP-Web

Length of output: 709


์‹คํŒจ ๊ฒฝ๋กœ์—์„œ 500ms ์ตœ์†Œ ๋กœ๋”ฉ ์‹œ๊ฐ„์ด ๋ณด์žฅ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

Line 65์—์„œ Promise.all([getRecentSearch('ROOM'), minLoadingTime])์€ ์–ด๋А ํ•œ Promise๊ฐ€ reject๋˜๋ฉด ์ฆ‰์‹œ ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค. ์š”์ฒญ์ด ์‹คํŒจํ•˜๋ฉด minLoadingTime์ด ์™„๋ฃŒ๋˜์ง€ ์•Š์€ ์ƒํƒœ์—์„œ catch ๋ธ”๋ก(67-68)์œผ๋กœ ๋น ์ ธ ์ตœ์†Œ ๋กœ๋”ฉ ์‹œ๊ฐ„์„ ๋ณด์žฅํ•˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค.

Line 94-104๋„ ๋™์ผํ•œ ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. Promise.all([getSearchRooms(...)])์—๋Š” minLoadingTime์ด ํฌํ•จ๋˜์ง€ ์•Š์•„, ์š”์ฒญ์ด ์‹คํŒจํ•˜๋ฉด catch ๋ธ”๋ก(114-115)์œผ๋กœ ์ฆ‰์‹œ ์ด๋™ํ•˜์—ฌ 104๋ฒˆ ์ค„์˜ await minLoadingTime์— ๋„๋‹ฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๊ฒฐ๊ณผ์ ์œผ๋กœ ์„ฑ๊ณต ์ผ€์ด์Šค์™€ ์‹คํŒจ ์ผ€์ด์Šค์˜ ๋กœ๋”ฉ UX๊ฐ€ ๋ถˆ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”ง ์ œ์•ˆ ์ˆ˜์ •์•ˆ
   const fetchRecentSearches = async () => {
     try {
       setIsLoadingRecentSearches(true);
       const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500));
-      const [response] = await Promise.all([getRecentSearch('ROOM'), minLoadingTime]);
-      setRecentSearches(response.isSuccess ? response.data.recentSearchList : []);
+      const request = getRecentSearch('ROOM')
+        .then(response => ({ ok: true as const, response }))
+        .catch(() => ({ ok: false as const }));
+      const [result] = await Promise.all([request, minLoadingTime]);
+      setRecentSearches(
+        result.ok && result.response.isSuccess ? result.response.data.recentSearchList : [],
+      );
     } catch {
       setRecentSearches([]);
     } finally {
       setIsLoadingRecentSearches(false);
     }
   };
       try {
         const isFinalized = status === 'searched';
         const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500));
-        const [res] = await Promise.all([
-          getSearchRooms(
-            term.trim(),
-            sortKey,
-            undefined,
-            isFinalized,
-            categoryParam,
-            isAllCategory,
-          ),
-        ]);
-        await minLoadingTime;
+        const request = getSearchRooms(
+          term.trim(),
+          sortKey,
+          undefined,
+          isFinalized,
+          categoryParam,
+          isAllCategory,
+        )
+          .then(res => ({ ok: true as const, res }))
+          .catch(error => ({ ok: false as const, error }));
+        const [result] = await Promise.all([request, minLoadingTime]);
+        if (!result.ok) throw result.error;
+        const res = result.res;
         if (res.isSuccess) {
           const { roomList, nextCursor: nc, isLast: last } = res.data;
๐Ÿ“ 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 minLoadingTime = new Promise(resolve => setTimeout(resolve, 500));
const [response] = await Promise.all([getRecentSearch('ROOM'), minLoadingTime]);
setRecentSearches(response.isSuccess ? response.data.recentSearchList : []);
const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500));
const request = getRecentSearch('ROOM')
.then(response => ({ ok: true as const, response }))
.catch(() => ({ ok: false as const }));
const [result] = await Promise.all([request, minLoadingTime]);
setRecentSearches(
result.ok && result.response.isSuccess ? result.response.data.recentSearchList : [],
);
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/groupSearch/GroupSearch.tsx` around lines 64 - 66, The current
Promise.all usage with minLoadingTime allows early rejection and skips the 500ms
delay on failures; change both places to use Promise.allSettled (or otherwise
ensure minLoadingTime is always awaited). For the getRecentSearch flow, replace
Promise.all([getRecentSearch('ROOM'), minLoadingTime]) with
Promise.allSettled([getRecentSearch('ROOM'), minLoadingTime]) and then check the
settled result for the getRecentSearch entry (status === 'fulfilled' and
value.isSuccess) before calling setRecentSearches, defaulting to [] on failure;
remove the later separate await minLoadingTime if present. Do the same for the
getSearchRooms call: include minLoadingTime in
Promise.allSettled([getSearchRooms(...), minLoadingTime]) and extract the rooms
only if the getSearchRooms settled entry succeeded, otherwise handle the error
path after the delay so success and failure both respect the 500ms minimum.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant