Skip to content

Feature/bookmark#8

Merged
hannah0352 merged 4 commits intomainfrom
feature/bookmark
Sep 19, 2025
Merged

Feature/bookmark#8
hannah0352 merged 4 commits intomainfrom
feature/bookmark

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.

@hannah0352 hannah0352 merged commit 12c2da4 into main Sep 19, 2025
1 check passed
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.

1 participant