Skip to content

Conversation

@hannah0352
Copy link
Collaborator

@hannah0352 hannah0352 commented Sep 19, 2025

๐Ÿ“Œ ์ž‘์—… ๋‚ด์šฉ

  • ์นด๋“œ/๋ฆฌ์ŠคํŠธ ๋ชจ๋“œ
  • ๋ถ๋งˆํฌ ์กฐํšŒ/์ถ”๊ฐ€/์ˆ˜์ •/์‚ญ์ œ
  • ๋ถ๋งˆํฌ ํด๋ฆญํ•˜๋ฉด ์„œ๋ฒ„ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ์ƒˆ ์ฐฝ์—์„œ ํ™•์ธ

๐Ÿ“ธ ์Šคํฌ๋ฆฐ์ƒท

แ„‰แ…ณแ„แ…ณแ„…แ…ตแ†ซแ„‰แ…ฃแ†บ 2025-09-20 แ„‹แ…ฉแ„Œแ…ฅแ†ซ 12 27 14

๐Ÿ“ ๊ธฐํƒ€

Summary by CodeRabbit

  • ์‹ ๊ธฐ๋Šฅ

    • ๋ถ๋งˆํฌ ํŽ˜์ด์ง€ ์ถ”๊ฐ€: ๋กœ๊ทธ์ธ/ํšŒ์›๊ฐ€์ž… ๋ฐ ๋กœ๊ทธ์ธ ์œ ์ง€, ๋ชฉ๋ก ๋กœ๋”ฉยท์˜ค๋ฅ˜ ํ‘œ์‹œ, ๊ฒ€์ƒ‰ยทํ•„ํ„ฐ, ๊ทธ๋ฆฌ๋“œ/๋ฆฌ์ŠคํŠธ ์ „ํ™˜, ํ•ญ๋ชฉ ์ถ”๊ฐ€ยท์ˆ˜์ •ยท์‚ญ์ œ(๋ชจ๋‹ฌยทํ™•์ธ) ์ง€์›, ํ•ญ๋ชฉ ํด๋ฆญ ์‹œ ์‹œ๊ฐ„ ํ™•์ธ ํ๋ฆ„์œผ๋กœ ๊ฒฐ๊ณผ ํŽ˜์ด์ง€ ์ƒˆ ํƒญ ์—ด๊ธฐ, ํŒŒ๋น„์ฝ˜ ํ‘œ์‹œ ๋ฐ ๋Œ€์ฒด ์ฒ˜๋ฆฌ ํฌํ•จ.
  • ๋‚ด๋น„๊ฒŒ์ด์…˜

    • ํ—ค๋”์˜ โ€˜๋ถ๋งˆํฌโ€™ ๋ฉ”๋‰ด๊ฐ€ ์‹ค์ œ /bookmarks ๊ฒฝ๋กœ๋กœ ์ด๋™ํ•˜๋„๋ก ์—…๋ฐ์ดํŠธ.

@hannah0352 hannah0352 added the feat๐Ÿ› ๏ธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ label Sep 19, 2025
@hannah0352 hannah0352 self-assigned this Sep 19, 2025
@coderabbitai
Copy link

coderabbitai bot commented Sep 19, 2025

Walkthrough

ํด๋ผ์ด์–ธํŠธ ๋ถ๋งˆํฌ ํŽ˜์ด์ง€์™€ ๊ด€๋ จ UI ์ปดํฌ๋„ŒํŠธ, BookmarkAPI, ์ธ์ฆ ์œ ํ‹ธ(AuthUtils) ๋ฐ ํƒ€์ž… ์ •์˜๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  ํ—ค๋”์˜ ๋ถ๋งˆํฌ ๋งํฌ๋ฅผ /bookmarks๋กœ ์—ฐ๊ฒฐํ–ˆ๋‹ค. ๋ถ๋งˆํฌ CRUD, ๊ฒ€์ƒ‰/๋ณด๊ธฐ ์ „ํ™˜, ํด๋ฆญ(์‹œ๊ฐ„ ํ™•์ธ) ํ๋ฆ„๊ณผ ๋กœ๊ทธ์ธ/ํšŒ์›๊ฐ€์ž… ๋ชจ๋‹ฌ์„ ๊ตฌํ˜„ํ–ˆ๋‹ค.

Changes

Cohort / File(s) Summary
Bookmarks Page
src/app/bookmarks/page.tsx
์‹ ๊ทœ ํด๋ผ์ด์–ธํŠธ ํŽ˜์ด์ง€ ์ถ”๊ฐ€. ๋ถ๋งˆํฌ ์ƒํƒœ/๊ฒ€์ƒ‰/๋ทฐ๋ชจ๋“œ, ๋กœ๋”ฉยท์—๋Ÿฌ ์ฒ˜๋ฆฌ, ์ธ์ฆ ์ƒํƒœ(localStorage) ์—ฐ๋™, CRUDยทํด๋ฆญ ํ™•์ธ ํ๋ฆ„ ๋ฐ ๋กœ๊ทธ์ธ/ํšŒ์›๊ฐ€์ž…/ํ™•์ธ ๋ชจ๋‹ฌ ํ†ตํ•ฉ, API ํ˜ธ์ถœ๊ณผ ๊ฒฐ๊ณผ ํŽ˜์ด์ง€ ์˜คํ”ˆ ๊ตฌํ˜„.
Bookmark UI Components
src/components/bookmarks/BookmarkForm.tsx, src/components/bookmarks/BookmarkItem.tsx, src/components/bookmarks/BookmarkList.tsx, src/components/bookmarks/BookmarkModal.tsx
๋ถ๋งˆํฌ ํผ(๊ฒ€์ฆยท์ œ์ถœ), ํ•ญ๋ชฉ ๋ Œ๋”๋Ÿฌ(๋ฆฌ์ŠคํŠธ/๊ทธ๋ฆฌ๋“œ), ๋ชฉ๋ก ๋กœ๋“œ ๋ฐ CRUD ๋กœ์ง(๋ชจ๋‹ฌ ๊ธฐ๋ฐ˜ ์ถ”๊ฐ€/์ˆ˜์ •, ์‚ญ์ œ ํ™•์ธ), ํด๋ฆญ(์‹œ๊ฐ„ ํ™•์ธ) ํ๋ฆ„๊ณผ ๋ชจ๋‹ฌ ESC/๋ฐฑ๋“œ๋กญ ๋‹ซ๊ธฐ ์ฒ˜๋ฆฌ ์ถ”๊ฐ€.
API Client
src/libs/api/bookmarks.ts
BookmarkAPI ํด๋ž˜์Šค ์ถ”๊ฐ€: getBookmarks, createBookmark, updateBookmark, deleteBookmark, clickBookmark ๊ตฌํ˜„. NEXT_PUBLIC_API_BASE ์‚ฌ์šฉ, AuthUtils ํ—ค๋” ์ ์šฉ, 401 ๋ฐ ๋‹ค์–‘ํ•œ ์‘๋‹ต ํฌ๋งท ์ •๊ทœํ™” ๋ฐ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ.
Auth Utilities
src/libs/auth.ts
AuthUtils ์ถ”๊ฐ€: getToken/setToken/removeToken/hasToken/getAuthHeaders(SSR ๊ฐ€๋“œ ํฌํ•จ)๋กœ ๋กœ์ปฌ์Šคํ† ๋ฆฌ์ง€ ๊ธฐ๋ฐ˜ ํ† ํฐ ๊ด€๋ฆฌ ๋ฐ ์ธ์ฆ ํ—ค๋” ์ƒ์„ฑ.
Types
src/types/bookmark.ts
Bookmark, BookmarkCreateRequest, BookmarkUpdateRequest, BookmarkFormData ์ธํ„ฐํŽ˜์ด์Šค ์ถ”๊ฐ€.
Header Nav
src/components/ui/Header.tsx
ํ—ค๋”์˜ ๋ถ๋งˆํฌ ๋‚ด๋น„๊ฒŒ์ด์…˜ ๋งํฌ๋ฅผ #์—์„œ /bookmarks๋กœ ๋ณ€๊ฒฝ.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Page as BookmarksPage
  participant Components as UI Components
  participant API as BookmarkAPI
  participant Auth as AuthUtils

  Note over Page: ํŽ˜์ด์ง€ ์ง„์ž…
  User->>Page: /bookmarks ์š”์ฒญ
  Page->>Auth: hasToken/getToken
  Page->>API: getBookmarks()
  API-->>Page: Bookmark[]

  Note over Components: ๋ชฉ๋ก ํ‘œ์‹œ & ์ƒํ˜ธ์ž‘์šฉ
  User->>Components: ๊ฒ€์ƒ‰/๋ทฐ ์ „ํ™˜/์•„์ดํ…œ ํด๋ฆญ

  alt ๋ถ๋งˆํฌ ์ถ”๊ฐ€/์ˆ˜์ •
    User->>Components: Add/Edit ํด๋ฆญ
    Components->>Components: BookmarkModal ์—ด๊ธฐ
    User->>Components: ์ œ์ถœ
    Components->>Auth: getAuthHeaders
    Components->>API: createBookmark/updateBookmark
    API-->>Components: Bookmark
    Components->>Page: ๋ชฉ๋ก ๊ฐฑ์‹ 
  end

  alt ์‚ญ์ œ
    User->>Components: ์‚ญ์ œ ํด๋ฆญ
    Components->>API: deleteBookmark
    API-->>Components: ok
    Components->>Page: ๋ชฉ๋ก์—์„œ ์ œ๊ฑฐ
  end

  alt ํด๋ฆญ(์‹œ๊ฐ„ ํ™•์ธ)
    User->>Components: ์•„์ดํ…œ ํด๋ฆญ
    Components->>Components: ConfirmModal
    User->>Components: ํ™•์ธ
    Components->>API: clickBookmark
    API-->>Components: ok
    Components->>User: /result?url=... ์ƒˆ ํƒญ ์˜คํ”ˆ
  end

  alt ๋น„๋กœ๊ทธ์ธ ์‹œ ๋ณดํ˜ธ ๋™์ž‘
    User->>Components: ๋ณดํ˜ธ๋œ ์ž‘์—… ์‹œ๋„
    Components->>Components: LoginModal/SignupModal ํ‘œ์‹œ
    User->>Components: ๋กœ๊ทธ์ธ/ํšŒ์›๊ฐ€์ž… ์™„๋ฃŒ
    Components->>Auth: setToken
    Components->>API: getBookmarks()
  end
Loading

Estimated code review effort

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

Poem

ํ† ๋ผ๊ฐ€ ํผ์— ๊นก์ด, ๋งํฌ๋ฅผ ์ •์„ฑ์Šค๋ ˆ ๋‹ด๊ณ 
์ฐฝ์€ ํƒ ์—ด๋ ค, ์‹œ๊ฐ„์€ ํ†ก ์ƒˆ ํƒญ์œผ๋กœ ๋‚ ์•„๊ฐ€๋„ค
ํ† ํฐ์€ ์ฃผ๋จธ๋‹ˆ์—, ๋ชจ๋‹ฌ์€ ์‚ดํฌ์‹œ ๋‹ซํžˆ๋„ค
๋ชฉ๋ก์€ ๋ฐ˜์ง, ๋ฒ„ํŠผ์€ ์„ฑ์‹คํžˆ ๋ˆŒ๋Ÿฌ์ ธ
๋‚˜๋Š” ํ† ๋ผ, ๋ถ๋งˆํฌ ์ˆฒ์—์„œ ๋˜ ํ•œ๊ฑธ์Œ ๋›ฐ๋„ค ๐Ÿ‡โœจ

Pre-merge checks and finishing touches

โŒ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage โš ๏ธ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
โœ… Passed checks (2 passed)
Check name Status Explanation
Title Check โœ… Passed PR ์ œ๋ชฉ "Feature/bookmark"์€ ๋ณ€๊ฒฝ์‚ฌํ•ญ์˜ ํ•ต์‹ฌ์ธ ๋ถ๋งˆํฌ ๊ธฐ๋Šฅ ์ถ”๊ฐ€๋ฅผ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ๋‚˜ํƒ€๋‚ด์–ด ๋ณ€๊ฒฝ ๋‚ด์šฉ๊ณผ ์ง์ ‘์ ์œผ๋กœ ๊ด€๋ จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋งŒ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ์˜ PR ์ œ๋ชฉ ํ…œํ”Œ๋ฆฟ(์˜ˆ: [feature/#์ด์Šˆ๋ฒˆํ˜ธ] ์„ค๋ช…)์„ ๋”ฐ๋ฅด์ง€ ์•Š์œผ๋ฉฐ ๊ตฌ์ฒด์„ฑ์ด ๋ถ€์กฑํ•ด ํžˆ์Šคํ† ๋ฆฌ์—์„œ ๋น ๋ฅด๊ฒŒ ํŒŒ์•…ํ•˜๊ธฐ๋Š” ์–ด๋ ต์Šต๋‹ˆ๋‹ค. ์ „์ฒด์ ์œผ๋กœ ๊ด€๋ จ์„ฑ์€ ์žˆ์œผ๋‚˜ ํ˜•์‹๊ณผ ๋ช…ํ™•์„ฑ์—์„œ ๊ฐœ์„  ์—ฌ์ง€๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.
Description Check โœ… Passed PR ์„ค๋ช…์€ ํ…œํ”Œ๋ฆฟ์˜ "๐Ÿ“Œ ์ž‘์—… ๋‚ด์šฉ", "๐Ÿ“ธ ์Šคํฌ๋ฆฐ์ƒท", "๐Ÿ“ ๊ธฐํƒ€" ์„น์…˜ ๊ตฌ์กฐ๋ฅผ ๋”ฐ๋ฅด๊ณ  ์žˆ์œผ๋ฉฐ ์นด๋“œ/๋ฆฌ์ŠคํŠธ ์ „ํ™˜, ๋ถ๋งˆํฌ ์กฐํšŒยท์ถ”๊ฐ€ยท์ˆ˜์ •ยท์‚ญ์ œ, ํด๋ฆญ ์‹œ ๊ฒฐ๊ณผ ์ƒˆ ์ฐฝ ์˜คํ”ˆ ๋“ฑ ์ฃผ์š” ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ๋ช…ํ™•ํžˆ ๋‚˜์—ดํ•˜๊ณ  ์žˆ์–ด ์ „๋ฐ˜์ ์œผ๋กœ ์ถฉ๋ถ„ํžˆ ์ž‘์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋งŒ "๐Ÿ“ ๊ธฐํƒ€" ์„น์…˜์ด ๋น„์–ด ์žˆ์–ด ๋ฆฌ๋ทฐ ์ฐธ๊ณ ์‚ฌํ•ญ์ด๋‚˜ ๋…ผ์˜ ํฌ์ธํŠธ๊ฐ€ ์žˆ๋‹ค๋ฉด ๋ณด์™„ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ ํ…œํ”Œ๋ฆฟ ์•ˆ๋‚ด๋Œ€๋กœ ๋ ˆ์ด๋ธ” ์ ์šฉ ์—ฌ๋ถ€์™€ ์—ฐ๊ฒฐ๋œ ์ด์Šˆ ๋ฒˆํ˜ธ๋ฅผ ๋ณธ๋ฌธ์— ๋ช…์‹œํ•˜๋ฉด ๋ฆฌ๋ทฐ๊ฐ€ ๋” ์ˆ˜์›”ํ•ด์ง‘๋‹ˆ๋‹ค.
โœจ Finishing touches
  • ๐Ÿ“ Generate Docstrings
๐Ÿงช Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/bookmark

๐Ÿ“œ Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between 04a4a81 and dcd6de9.

๐Ÿ“’ Files selected for processing (2)
  • src/app/bookmarks/page.tsx (1 hunks)
  • src/components/bookmarks/BookmarkList.tsx (1 hunks)
๐Ÿšง Files skipped from review as they are similar to previous changes (2)
  • src/app/bookmarks/page.tsx
  • src/components/bookmarks/BookmarkList.tsx

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

๐Ÿงน Nitpick comments (18)
src/components/bookmarks/BookmarkItem.tsx (2)

20-27: favicon URL ์ƒ์„ฑ ๋กœ์ง ๊ฐœ์„  ํ•„์š”

ํ˜„์žฌ ๋ชจ๋“  ๋„๋ฉ”์ธ์—์„œ /favicon.ico ๊ฒฝ๋กœ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜๊ณ  ์žˆ์ง€๋งŒ, ์‹ค์ œ๋กœ๋Š” ๋งŽ์€ ์‚ฌ์ดํŠธ๋“ค์ด ๋‹ค๋ฅธ ๊ฒฝ๋กœ๋‚˜ ํ˜•์‹์˜ favicon์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ bookmark.favicon ํ•„๋“œ๊ฐ€ ์žˆ์Œ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ์‚ฌ์šฉ๋˜์ง€ ์•Š๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

 const getFaviconUrl = (url: string) => {
   try {
     const urlObj = new URL(url);
-    return `${urlObj.origin}/favicon.ico`;
+    // bookmark.favicon์ด ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ
+    if (bookmark.favicon) {
+      // ์ ˆ๋Œ€ URL์ธ์ง€ ํ™•์ธ
+      try {
+        new URL(bookmark.favicon);
+        return bookmark.favicon;
+      } catch {
+        // ์ƒ๋Œ€ ๊ฒฝ๋กœ์ธ ๊ฒฝ์šฐ origin๊ณผ ๊ฒฐํ•ฉ
+        return `${urlObj.origin}${bookmark.favicon.startsWith('/') ? '' : '/'}${bookmark.favicon}`;
+      }
+    }
+    // ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ /favicon.ico ์‚ฌ์šฉ
+    return `${urlObj.origin}/favicon.ico`;
   } catch {
     return null;
   }
 };

47-51: ์ด๋ฏธ์ง€ ์—๋Ÿฌ ํ•ธ๋“ค๋ง์—์„œ DOM ์ง์ ‘ ์กฐ์ž‘ ์ง€์–‘

onError ํ•ธ๋“ค๋Ÿฌ์—์„œ DOM์„ ์ง์ ‘ ์กฐ์ž‘ํ•˜๋Š” ๋Œ€์‹  React state๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๋” ์•ˆ์ „ํ•˜๊ณ  React ํŒจ๋Ÿฌ๋‹ค์ž„์— ๋งž์Šต๋‹ˆ๋‹ค.

+const [faviconError, setFaviconError] = useState(false);

 {bookmark.favicon && faviconUrl ? (
   <img 
     src={faviconUrl} 
     alt={bookmark.custom_name}
     className="w-8 h-8 rounded"
-    onError={(e) => {
-      e.currentTarget.style.display = 'none';
-      const nextElement = e.currentTarget.nextElementSibling as HTMLElement;
-      if (nextElement) nextElement.style.display = 'block';
-    }}
+    onError={() => setFaviconError(true)}
+    style={{ display: faviconError ? 'none' : 'block' }}
   />
 ) : null}
-<span style={{ display: bookmark.favicon && faviconUrl ? 'none' : 'block' }}>
+<span style={{ display: !bookmark.favicon || !faviconUrl || faviconError ? 'block' : 'none' }}>
 </span>

Also applies to: 109-113

src/types/bookmark.ts (1)

12-28: ์ค‘๋ณต๋œ ํƒ€์ž… ์ •์˜ ํ†ตํ•ฉ ๊ณ ๋ ค

BookmarkCreateRequest, BookmarkUpdateRequest, BookmarkFormData ์„ธ ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ๋™์ผํ•œ ํ•„๋“œ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ํƒ€์ž… ๋ณ„์นญ์ด๋‚˜ ์ƒ์†์„ ํ†ตํ•ด ์ค‘๋ณต์„ ์ค„์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

-export interface BookmarkCreateRequest {
-  custom_name: string;
-  custom_url: string;
-  favicon?: string; // ์„ ํƒ์  ํ•„๋“œ๋กœ ์œ ์ง€ (๋ฐฑ์—”๋“œ์—์„œ ์‚ฌ์šฉ)
-}
-
-export interface BookmarkUpdateRequest {
-  custom_name: string;
-  custom_url: string;
-  favicon?: string;
-}
-
-export interface BookmarkFormData {
-  custom_name: string;
-  custom_url: string;
-  favicon?: string;
-}
+export interface BookmarkFormData {
+  custom_name: string;
+  custom_url: string;
+  favicon?: string;
+}
+
+export type BookmarkCreateRequest = BookmarkFormData;
+export type BookmarkUpdateRequest = BookmarkFormData;
src/components/bookmarks/BookmarkForm.tsx (1)

54-61: URL ๊ฒ€์ฆ ๋กœ์ง ๊ฐœ์„  ํ•„์š”

ํ˜„์žฌ URL ๊ฒ€์ฆ์ด ๋„ˆ๋ฌด ์—„๊ฒฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ”„๋กœํ† ์ฝœ ์—†๋Š” URL(์˜ˆ: example.com)๋„ ํ—ˆ์šฉํ•˜๊ณ  ์ž๋™์œผ๋กœ https://๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์— ๋” ์ข‹์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 const isValidUrl = (url: string): boolean => {
   try {
+    // ํ”„๋กœํ† ์ฝœ์ด ์—†์œผ๋ฉด https:// ์ถ”๊ฐ€
+    if (!url.match(/^https?:\/\//)) {
+      url = `https://${url}`;
+    }
     new URL(url);
     return true;
   } catch {
     return false;
   }
 };

+// handleChange ํ•จ์ˆ˜๋„ ์ˆ˜์ •
+const handleChange = (field: keyof BookmarkFormData, value: string) => {
+  // URL ํ•„๋“œ์ธ ๊ฒฝ์šฐ ํ”„๋กœํ† ์ฝœ ์ž๋™ ์ถ”๊ฐ€
+  if (field === 'custom_url' && value && !value.match(/^https?:\/\//)) {
+    value = `https://${value}`;
+  }
+  setFormData((prev: BookmarkFormData) => ({ ...prev, [field]: value }));
+  // ...
+};
src/components/bookmarks/BookmarkModal.tsx (2)

95-99: any ํƒ€์ž… ์‚ฌ์šฉ ์ง€์–‘

TypeScript์˜ ํƒ€์ž… ์•ˆ์ •์„ฑ์„ ์œ„ํ•ด any ๋Œ€์‹  ๋ช…ํ™•ํ•œ ํƒ€์ž…์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

-    const submitData: any = {
+    const submitData: BookmarkFormData = {
       custom_name: formData.custom_name.trim(),
       custom_url: formData.custom_url.trim(),
       // favicon ํ•„๋“œ๋Š” ์ „์†กํ•˜์ง€ ์•Š์Œ (๋ฐฑ์—”๋“œ์—์„œ ์ž๋™ ์ฒ˜๋ฆฌ)
     };

80-87: BookmarkForm ์ปดํฌ๋„ŒํŠธ์˜ URL ๊ฒ€์ฆ ๋กœ์ง๊ณผ ์ค‘๋ณต

isValidUrl ํ•จ์ˆ˜๊ฐ€ BookmarkForm ์ปดํฌ๋„ŒํŠธ์—๋„ ๋™์ผํ•˜๊ฒŒ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. ๊ณตํ†ต ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜๋กœ ์ถ”์ถœํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

+// src/utils/validation.ts ํŒŒ์ผ ์ƒ์„ฑ
+export const isValidUrl = (url: string): boolean => {
+  try {
+    new URL(url);
+    return true;
+  } catch {
+    return false;
+  }
+};

๊ทธ๋ฆฌ๊ณ  ๋‘ ์ปดํฌ๋„ŒํŠธ์—์„œ importํ•˜์—ฌ ์‚ฌ์šฉ:

+import { isValidUrl } from '@/utils/validation';

-const isValidUrl = (url: string): boolean => {
-  try {
-    new URL(url);
-    return true;
-  } catch {
-    return false;
-  }
-};
src/components/bookmarks/BookmarkList.tsx (2)

77-91: ์‹œ๊ฐ„ ํ™•์ธ ์‹œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๊ฐœ์„  ํ•„์š”

window.open์ด ํŒ์—… ์ฐจ๋‹จ๊ธฐ์— ์˜ํ•ด ์ฐจ๋‹จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ  ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ ค์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 const executeCheckTime = async () => {
   if (!selectedBookmark) return;
   
   try {
     await BookmarkAPI.clickBookmark(selectedBookmark.id);
     // ์‹œ๊ฐ„ ํ™•์ธ ๊ฒฐ๊ณผ๋ฅผ ์ƒˆ ์ฐฝ์—์„œ ์—ด๊ธฐ
-    window.open(`/result?url=${encodeURIComponent(selectedBookmark.custom_url)}`, '_blank');
+    const newWindow = window.open(`/result?url=${encodeURIComponent(selectedBookmark.custom_url)}`, '_blank');
+    if (!newWindow) {
+      alert('ํŒ์—…์ด ์ฐจ๋‹จ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ํŒ์—… ์ฐจ๋‹จ์„ ํ•ด์ œํ•ด ์ฃผ์„ธ์š”.');
+    }
     setConfirmModalOpen(false);
     setSelectedBookmark(null);
   } catch (err) {
     alert(err instanceof Error ? err.message : '์‹œ๊ฐ„ ํ™•์ธ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
     setConfirmModalOpen(false);
     setSelectedBookmark(null);
   }
 };

175-186: ๋ถ๋งˆํฌ๊ฐ€ ์—†์„ ๋•Œ ๋นˆ ์ƒํƒœ UI ์ถ”๊ฐ€ ๊ถŒ์žฅ

๋ถ๋งˆํฌ ๋ชฉ๋ก์ด ๋น„์–ด์žˆ์„ ๋•Œ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋” ๋‚˜์€ ๊ฒฝํ—˜์„ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•ด ๋นˆ ์ƒํƒœ UI๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

 {/* ๋ถ๋งˆํฌ ๋ชฉ๋ก */}
+{bookmarks.length === 0 ? (
+  <div className="text-center py-12">
+    <svg className="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
+    </svg>
+    <h3 className="mt-2 text-sm font-medium text-gray-900">๋ถ๋งˆํฌ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค</h3>
+    <p className="mt-1 text-sm text-gray-500">์ƒˆ๋กœ์šด ๋ถ๋งˆํฌ๋ฅผ ์ถ”๊ฐ€ํ•ด ๋ณด์„ธ์š”.</p>
+    <div className="mt-6">
+      <button
+        onClick={handleAdd}
+        className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
+      >
+        ๋ถ๋งˆํฌ ์ถ”๊ฐ€
+      </button>
+    </div>
+  </div>
+) : (
   <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
     {bookmarks.map((bookmark) => (
       <BookmarkItem
         key={bookmark.id}
         bookmark={bookmark}
         onEdit={handleEdit}
         onDelete={handleDelete}
         onCheckTime={handleCheckTime}
         viewMode="grid"
       />
     ))}
   </div>
+)}
src/libs/auth.ts (1)

17-22: ํ† ํฐ ์ €์žฅ/์‚ญ์ œ ์ผ๊ด€์„ฑ ํ•„์š” โ€” AuthUtils์— ์ €์žฅ ๋กœ์ง ํ†ตํ•ฉ ๊ถŒ์žฅ

๋ฐœ๊ฒฌ: refreshTokenยทuserName์€ src/app/page.tsx(๋ผ์ธ 110โ€“113) ๋ฐ src/app/bookmarks/page.tsx(๋ผ์ธ 364โ€“367)์—์„œ localStorage์— ์ง์ ‘ ์ €์žฅ๋˜๋Š” ๋ฐ˜๋ฉด, src/libs/auth.ts์˜ AuthUtils.setToken์€ accessToken๋งŒ ์ €์žฅ(setToken: src/libs/auth.ts:11โ€“13). removeToken์€ accessTokenยทrefreshTokenยทuserName์„ ๋ชจ๋‘ ์ œ๊ฑฐ(src/libs/auth.ts:17โ€“21).

๊ถŒ์žฅ: ์ €์žฅ/์‚ญ์ œ ๋กœ์ง์„ ์ค‘์•™ํ™”(์˜ˆ: AuthUtils.setToken(token, refreshToken?, userName?))ํ•˜๊ฑฐ๋‚˜ removeToken๊ณผ setToken์ด ๋™์ผํ•œ ํ‚ค๋ฅผ ๋‹ค๋ฃจ๋„๋ก ๋งž์ถœ ๊ฒƒ.

src/app/bookmarks/page.tsx (5)

103-108: ๋น„๋กœ๊ทธ์ธ ์ƒํƒœ์—์„œ ์‹œ๊ฐ„ ํ™•์ธ ์‹œ ๋กœ๊ทธ์ธ ์œ ๋„ ํ•„์š”

ํ˜„์žฌ๋Š” ๋น„๋กœ๊ทธ์ธ๋„ ํ™•์ธ ๋ชจ๋‹ฌ์„ ๋„์šด ํ›„ API ํ˜ธ์ถœ์—์„œ 401์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. handleAdd์™€ ๋™์ผํ•˜๊ฒŒ ํด๋ฆญ ์‹œ ๋กœ๊ทธ์ธ ๋ชจ๋‹ฌ์„ ์œ ๋„ํ•˜๋Š” ํŽธ์ด UX์™€ ์˜ค๋ฅ˜์œจ ์ธก๋ฉด์—์„œ ๋‚ซ์Šต๋‹ˆ๋‹ค.

 const handleCheckTime = (bookmark: Bookmark) => {
-  setSelectedBookmark(bookmark);
-  setConfirmOpen(true);
+  if (!isAuthed) {
+    setLoginOpen(true);
+    return;
+  }
+  setSelectedBookmark(bookmark);
+  setConfirmOpen(true);
 };

91-101: ๋ธŒ๋ผ์šฐ์ € confirm ๋Œ€์‹  ๊ณต์šฉ ConfirmModal ์‚ฌ์šฉ์œผ๋กœ UX ์ผ๊ด€ํ™”

์‚ญ์ œ ํ™•์ธ์€ window.confirm ๋Œ€์‹  ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ConfirmModal์„ ์žฌ์‚ฌ์šฉํ•˜์„ธ์š”. ์ ‘๊ทผ์„ฑ/๋””์ž์ธ ์ผ๊ด€์„ฑ ํ™•๋ณด๋ฉ๋‹ˆ๋‹ค.

๊ฐ„๋‹จ ๋Œ€์•ˆ: ์‚ญ์ œ์šฉ ConfirmModal ์ƒํƒœ(์—ด๋ฆผ/๋Œ€์ƒ id) ์ถ”๊ฐ€ ํ›„, ํ™•์ธ ์‹œ BookmarkAPI.deleteBookmark ์‹คํ–‰.


171-189: ๋‚ด๋ถ€ ๋ผ์šฐํŒ…์€ Link ์‚ฌ์šฉ ๋ฐ ๋นˆ # ์ œ๊ฑฐ

/bookmarks๋Š” <a> ๋Œ€์‹  next/link๋ฅผ ์‚ฌ์šฉํ•ด ์ „์ฒด ๋ฆฌ๋กœ๋“œ๋ฅผ ํ”ผํ•˜์„ธ์š”. href="#"๋Š” ๋ถˆํ•„์š”ํ•œ ์ตœ์ƒ๋‹จ ์ด๋™์„ ์œ ๋ฐœํ•ฉ๋‹ˆ๋‹ค.

- <a href="/bookmarks" className="text-black text-sm font-semibold no-underline">๋ถ๋งˆํฌ</a>
+ <Link href="/bookmarks" className="text-black text-sm font-semibold no-underline">๋ถ๋งˆํฌ</Link>
- <a href="#" className="text-gray-600 text-sm font-medium hover:text-black transition-colors no-underline">์‹ค์‹œ๊ฐ„ ๋žญํ‚น</a>
+ <button className="text-gray-600 text-sm font-medium hover:text-black transition-colors">์‹ค์‹œ๊ฐ„ ๋žญํ‚น</button>

151-161: ํ† ํฐ ์‚ญ์ œ ๋กœ์ง ๋‹จ์ผํ™”

์ง์ ‘ localStorage ์กฐ์ž‘ ๋Œ€์‹  AuthUtils.removeToken()์„ ์‚ฌ์šฉํ•ด ์ค‘๋ณต/๋ˆ„๋ฝ ์œ„ํ—˜์„ ์ค„์ด์„ธ์š”.

-  localStorage.removeItem('accessToken');
-  localStorage.removeItem('refreshToken');
-  localStorage.removeItem('userName');
+  AuthUtils.removeToken();

349-377: ์ธ์ฆ API ํ˜ธ์ถœ ๋ถ„๋ฆฌ(์ค‘๋ณต ์ œ๊ฑฐ ๋ฐ ์‘๋‹ต ์Šคํ‚ค๋งˆ ํก์ˆ˜)

๋กœ๊ทธ์ธ/ํšŒ์›๊ฐ€์ž… fetch ๋กœ์ง์„ ํŽ˜์ด์ง€์— ์ง์ ‘ ๋‘๋ฉด ์‘๋‹ต ์Šคํ‚ค๋งˆ ๋ณ€๊ฒฝ์— ์ทจ์•ฝํ•ฉ๋‹ˆ๋‹ค. src/libs/api/auth.ts ๋“ฑ์œผ๋กœ ์ด๋™ํ•ด ์ผ๊ด€ ์ฒ˜๋ฆฌ(์—๋Ÿฌ ํŒŒ์‹ฑ, ํ† ํฐ ์ €์žฅ)ํ•˜์„ธ์š”.

Also applies to: 389-413

src/libs/api/bookmarks.ts (4)

178-198: ํด๋ฆญ API๋„ ๋นˆ/๋น„-JSON ์‘๋‹ต ๋Œ€๋น„ ํ•„์š”

๋ฆฌ๋‹ค์ด๋ ‰ํŠธ/204/ํ…์ŠคํŠธ ์‘๋‹ต์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. JSON ์ „์ œ ์ œ๊ฑฐ๊ฐ€ ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.

-    const result = await response.json();
-
-    // ํด๋ฆญ์€ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฏ€๋กœ ์„ฑ๊ณต ์—ฌ๋ถ€๋งŒ ํ™•์ธ
-    if (!result.success) {
-      throw new Error('๋ถ๋งˆํฌ ํด๋ฆญ ์ฒ˜๋ฆฌ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
-    }
+    const ct = response.headers.get('content-type') || '';
+    if (ct.includes('application/json')) {
+      const result = await response.json();
+      if (result && result.success === false) {
+        throw new Error('๋ถ๋งˆํฌ ํด๋ฆญ ์ฒ˜๋ฆฌ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
+      }
+    }

12-16: GET ์š”์ฒญ์— ๋ถˆํ•„์š”ํ•œ Content-Type ํ—ค๋” ์ œ๊ฑฐ

getAuthHeaders()๊ฐ€ ๋ชจ๋“  ์š”์ฒญ์— Content-Type: application/json์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. GET์—๋Š” ๋ถˆํ•„์š”ํ•˜๋ฉฐ ์ผ๋ถ€ ์„œ๋ฒ„์—์„œ ์—„๊ฒฉํžˆ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค. ์„ ํƒ์ ์œผ๋กœ ํฌํ•จํ•˜๋„๋ก ๋ณ€๊ฒฝ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.


42-56: ๊ด‘๋ฒ”์œ„ํ•œ console ๋กœ๊ทธ๋Š” ์ œ๊ฑฐ ๋˜๋Š” ๋””๋ฒ„๊ทธ ํ”Œ๋ž˜๊ทธ๋กœ ์ œํ•œ

PII/์šด์˜ ๋กœ๊ทธ ๊ณผ๋‹ค ๋…ธ์ถœ ์šฐ๋ ค. process.env.NODE_ENV !== 'production' ๊ฐ€๋“œ๋กœ ์ œํ•œํ•˜๊ฑฐ๋‚˜ ๋กœ๊ฑฐ๋กœ ๋Œ€์ฒดํ•˜์„ธ์š”.

Also applies to: 79-86, 90-106, 128-133


21-23: ์—๋Ÿฌ ๋ฉ”์‹œ์ง€์— ์ƒํƒœ์ฝ”๋“œ ํฌํ•จ(๋””๋ฒ„๊น… ์šฉ์ด์„ฑ ํ–ฅ์ƒ)

๊ณ ์ • ๋ฌธ๊ตฌ ๋Œ€์‹  ์ƒํƒœ์ฝ”๋“œ๋ฅผ ํฌํ•จํ•ด ์›์ธ ํŒŒ์•…์„ ๋น ๋ฅด๊ฒŒ ํ•˜์„ธ์š”.

-  throw new Error('๋ถ๋งˆํฌ ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
+  throw new Error(`๋ถ๋งˆํฌ ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. (status=${response.status})`);
๐Ÿ“œ Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between d969f64 and 04a4a81.

๐Ÿ“’ Files selected for processing (9)
  • src/app/bookmarks/page.tsx (1 hunks)
  • src/components/bookmarks/BookmarkForm.tsx (1 hunks)
  • src/components/bookmarks/BookmarkItem.tsx (1 hunks)
  • src/components/bookmarks/BookmarkList.tsx (1 hunks)
  • src/components/bookmarks/BookmarkModal.tsx (1 hunks)
  • src/components/ui/Header.tsx (1 hunks)
  • src/libs/api/bookmarks.ts (1 hunks)
  • src/libs/auth.ts (1 hunks)
  • src/types/bookmark.ts (1 hunks)
๐Ÿงฐ Additional context used
๐Ÿงฌ Code graph analysis (6)
src/components/bookmarks/BookmarkList.tsx (5)
src/types/bookmark.ts (2)
  • Bookmark (1-10)
  • BookmarkFormData (24-28)
src/libs/api/bookmarks.ts (1)
  • BookmarkAPI (10-199)
src/components/bookmarks/BookmarkItem.tsx (1)
  • BookmarkItem (13-153)
src/components/bookmarks/BookmarkModal.tsx (1)
  • BookmarkModal (15-214)
src/components/ui/ConfirmModal.tsx (1)
  • ConfirmModal (15-105)
src/app/bookmarks/page.tsx (7)
src/types/bookmark.ts (2)
  • Bookmark (1-10)
  • BookmarkFormData (24-28)
src/libs/api/bookmarks.ts (1)
  • BookmarkAPI (10-199)
src/components/bookmarks/BookmarkItem.tsx (1)
  • BookmarkItem (13-153)
src/components/bookmarks/BookmarkModal.tsx (1)
  • BookmarkModal (15-214)
src/components/auth/LoginModal.tsx (1)
  • LoginModal (15-174)
src/components/auth/SignupModal.tsx (1)
  • SignupModal (16-252)
src/components/ui/ConfirmModal.tsx (1)
  • ConfirmModal (15-105)
src/components/bookmarks/BookmarkModal.tsx (1)
src/types/bookmark.ts (2)
  • Bookmark (1-10)
  • BookmarkFormData (24-28)
src/components/bookmarks/BookmarkItem.tsx (1)
src/types/bookmark.ts (1)
  • Bookmark (1-10)
src/libs/api/bookmarks.ts (2)
src/types/bookmark.ts (3)
  • Bookmark (1-10)
  • BookmarkCreateRequest (12-16)
  • BookmarkUpdateRequest (18-22)
src/libs/auth.ts (1)
  • AuthUtils (3-37)
src/components/bookmarks/BookmarkForm.tsx (1)
src/types/bookmark.ts (2)
  • Bookmark (1-10)
  • BookmarkFormData (24-28)
๐Ÿ”‡ Additional comments (1)
src/components/ui/Header.tsx (1)

61-66: LGTM! ๋ถ๋งˆํฌ ํŽ˜์ด์ง€๋กœ์˜ ๋ผ์šฐํŒ…์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๊ธฐ์กด์˜ placeholder ๋งํฌ(#)๊ฐ€ ์‹ค์ œ ๋ถ๋งˆํฌ ํŽ˜์ด์ง€ ๊ฒฝ๋กœ(/bookmarks)๋กœ ์ ์ ˆํžˆ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

Comment on lines +110 to +119
const executeCheckTime = async () => {
if (!selectedBookmark) return;

try {
await BookmarkAPI.clickBookmark(selectedBookmark.id);
// ์‹œ๊ฐ„ ํ™•์ธ ๊ฒฐ๊ณผ๋ฅผ ์ƒˆ ์ฐฝ์—์„œ ์—ด๊ธฐ
window.open(`/result?url=${encodeURIComponent(selectedBookmark.custom_url)}`, '_blank');
setConfirmOpen(false);
setSelectedBookmark(null);
} catch (err) {
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue

ํŒ์—… ์ฐจ๋‹จ ํšŒํ”ผ: ์ƒˆ ํƒญ์„ ๋™๊ธฐ์ ์œผ๋กœ ๋จผ์ € ์—ด๊ณ  URL ์ „ํ™˜ํ•˜์„ธ์š”.

์‚ฌ์šฉ์ž ์ œ์Šค์ฒ˜(ํด๋ฆญ) ์ดํ›„ ๋„คํŠธ์›Œํฌ ๋Œ€๊ธฐ(await) ๋’ค์— window.open์„ ํ˜ธ์ถœํ•˜๋ฉด ๋ธŒ๋ผ์šฐ์ € ํŒ์—… ์ฐจ๋‹จ์— ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋™๊ธฐ์ ์œผ๋กœ ๋นˆ ํƒญ์„ ๋จผ์ € ์—ด๊ณ , ์š”์ฒญ ์„ฑ๊ณต ์‹œ ๊ทธ ํƒญ์˜ location์„ ๊ฐฑ์‹ ํ•˜์„ธ์š”.

-  try {
-    await BookmarkAPI.clickBookmark(selectedBookmark.id);
-    // ์‹œ๊ฐ„ ํ™•์ธ ๊ฒฐ๊ณผ๋ฅผ ์ƒˆ ์ฐฝ์—์„œ ์—ด๊ธฐ
-    window.open(`/result?url=${encodeURIComponent(selectedBookmark.custom_url)}`, '_blank');
-    setConfirmOpen(false);
-    setSelectedBookmark(null);
-  } catch (err) {
+  const newTab = window.open('about:blank', '_blank', 'noopener,noreferrer');
+  try {
+    await BookmarkAPI.clickBookmark(selectedBookmark.id);
+    const targetUrl = `/result?url=${encodeURIComponent(selectedBookmark.custom_url)}`;
+    if (newTab) newTab.location.href = targetUrl;
+    else window.open(targetUrl, '_blank', 'noopener,noreferrer');
+    setConfirmOpen(false);
+    setSelectedBookmark(null);
+  } catch (err) {
     alert(err instanceof Error ? err.message : '์‹œ๊ฐ„ ํ™•์ธ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
     setConfirmOpen(false);
     setSelectedBookmark(null);
-  }
+    if (newTab) newTab.close();
+  }

Committable suggestion skipped: line range outside the PR's diff.

๐Ÿค– Prompt for AI Agents
In src/app/bookmarks/page.tsx around lines 110 to 119, calling window.open after
awaiting BookmarkAPI.clickBookmark can trigger browser popup blockers; open a
new blank tab synchronously on the user gesture before the await, save the
window reference, then after the API call succeeds set that window's location to
the encoded result URL (and on failure close the opened tab or navigate to an
error page); ensure you still call setConfirmOpen(false) and
setSelectedBookmark(null) after success and handle errors by closing the tab and
reporting the failure.

Comment on lines +8 to +9
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE;

Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue

ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋ฏธ์„ค์ • ๋ฐฉ์–ด ๋กœ์ง ์ถ”๊ฐ€

NEXT_PUBLIC_API_BASE๊ฐ€ ๋น„์–ด์žˆ์œผ๋ฉด ๋Ÿฐํƒ€์ž„์— URL ํŒŒ์‹ฑ ์˜ค๋ฅ˜๊ฐ€ ๋‚ฉ๋‹ˆ๋‹ค. ๊ฐ ๋ฉ”์„œ๋“œ ์ง„์ž… ์‹œ ์กฐ๊ธฐ ๊ฒ€์ฆํ•˜์„ธ์š”.

-const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE;
+const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE;
+function ensureBaseUrl(): string {
+  if (!API_BASE_URL) {
+    throw new Error('ํ™˜๊ฒฝ๋ณ€์ˆ˜ NEXT_PUBLIC_API_BASE ๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.');
+  }
+  return API_BASE_URL;
+}

๊ทธ๋ฆฌ๊ณ  ๊ฐ fetch ํ˜ธ์ถœ์˜ ํ…œํ”Œ๋ฆฟ ๋ฆฌํ„ฐ๋Ÿด ์•ž์—์„œ const base = ensureBaseUrl(); ์‚ฌ์šฉ.

๐Ÿ“ 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 API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE;
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE;
function ensureBaseUrl(): string {
if (!API_BASE_URL) {
throw new Error('ํ™˜๊ฒฝ๋ณ€์ˆ˜ NEXT_PUBLIC_API_BASE ๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.');
}
return API_BASE_URL;
}
๐Ÿค– Prompt for AI Agents
In src/libs/api/bookmarks.ts around lines 8-9, the module currently reads
NEXT_PUBLIC_API_BASE directly which leads to runtime URL parsing errors if the
env var is empty; add a small helper ensureBaseUrl() that validates and
normalizes process.env.NEXT_PUBLIC_API_BASE (throwing a clear error if
missing/invalid) and then, inside each exported API method, call const base =
ensureBaseUrl(); at the start and use that base variable instead of directly
interpolating the template literal URL so all fetch calls use the validated
base.

Comment on lines +156 to +176
static async deleteBookmark(id: number): Promise<void> {
const response = await fetch(`${API_BASE_URL}/api/bookmarks/${id}`, {
method: 'DELETE',
headers: AuthUtils.getAuthHeaders(),
});

if (response.status === 401) {
throw new Error('๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋‹ค์‹œ ๋กœ๊ทธ์ธํ•ด์ฃผ์„ธ์š”.');
}

if (!response.ok) {
throw new Error('๋ถ๋งˆํฌ ์‚ญ์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
}

const result = await response.json();

// ์‚ญ์ œ๋Š” { success: true, data: [...] } ํ˜•ํƒœ๋กœ ๋ฐ˜ํ™˜ํ•˜์ง€๋งŒ ์„ฑ๊ณต์ด๋ฉด OK
if (!result.success) {
throw new Error('๋ถ๋งˆํฌ ์‚ญ์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
}
}
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue

DELETE 204/๋นˆ ์‘๋‹ต ์ฒ˜๋ฆฌ ๋ˆ„๋ฝ(๋Ÿฐํƒ€์ž„ ์˜ˆ์™ธ ๊ฐ€๋Šฅ)

์„ฑ๊ณต ์‹œ 204 No Content๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐฑ์—”๋“œ๋Š” ํ”ํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ๋Š” ๋ฌด์กฐ๊ฑด response.json()์„ ํ˜ธ์ถœํ•ด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ณธ๋ฌธ ์œ ๋ฌด๋ฅผ ํ™•์ธํ•˜์„ธ์š”.

   static async deleteBookmark(id: number): Promise<void> {
-    const response = await fetch(`${API_BASE_URL}/api/bookmarks/${id}`, {
+    const base = ensureBaseUrl();
+    const response = await fetch(`${base}/api/bookmarks/${id}`, {
       method: 'DELETE',
       headers: AuthUtils.getAuthHeaders(),
     });
@@
-    const result = await response.json();
-
-    // ์‚ญ์ œ๋Š” { success: true, data: [...] } ํ˜•ํƒœ๋กœ ๋ฐ˜ํ™˜ํ•˜์ง€๋งŒ ์„ฑ๊ณต์ด๋ฉด OK
-    if (!result.success) {
-      throw new Error('๋ถ๋งˆํฌ ์‚ญ์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
-    }
+    // 204 ๋˜๋Š” ๋นˆ ๋ณธ๋ฌธ ์ฒ˜๋ฆฌ
+    const contentType = response.headers.get('content-type') || '';
+    const hasBody = contentType.includes('application/json');
+    if (!hasBody) return;
+    const result = await response.json();
+    if (result && result.success === false) {
+      throw new Error('๋ถ๋งˆํฌ ์‚ญ์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
+    }
   }

Committable suggestion skipped: line range outside the PR's diff.

๐Ÿค– Prompt for AI Agents
In src/libs/api/bookmarks.ts around lines 156 to 176, the code always calls
response.json() which will throw on a 204 No Content; update the success
handling to first accept HTTP 204 (or response.ok with no body) as success and
only call response.json() when a body is present (e.g., status !== 204 and
headers/content-type indicates JSON or content-length > 0). If status is 204 or
response.ok with no body, return immediately; otherwise parse JSON and validate
result.success as before, and keep the existing 401 and non-ok checks.

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

Labels

feat๐Ÿ› ๏ธ ๊ธฐ๋Šฅ ๊ตฌํ˜„

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants