-
Notifications
You must be signed in to change notification settings - Fork 0
Feature/bookmark #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Walkthroughํด๋ผ์ด์ธํธ ๋ถ๋งํฌ ํ์ด์ง์ ๊ด๋ จ UI ์ปดํฌ๋ํธ, BookmarkAPI, ์ธ์ฆ ์ ํธ(AuthUtils) ๋ฐ ํ์
์ ์๋ฅผ ์ถ๊ฐํ๊ณ ํค๋์ ๋ถ๋งํฌ ๋งํฌ๋ฅผ Changes
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
Estimated code review effort๐ฏ 4 (Complex) | โฑ๏ธ ~60 minutes Poem
Pre-merge checks and finishing touchesโ Failed checks (1 warning)
โ Passed checks (2 passed)
โจ Finishing touches
๐งช Generate unit tests
๐ Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro ๐ Files selected for processing (2)
๐ง Files skipped from review as they are similar to previous changes (2)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
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
๐ 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)๋ก ์ ์ ํ ๋ณ๊ฒฝ๋์์ต๋๋ค.
| 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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ํ์ ์ฐจ๋จ ํํผ: ์ ํญ์ ๋๊ธฐ์ ์ผ๋ก ๋จผ์ ์ด๊ณ 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.
| const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ํ๊ฒฝ๋ณ์ ๋ฏธ์ค์ ๋ฐฉ์ด ๋ก์ง ์ถ๊ฐ
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.
| 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.
| 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('๋ถ๋งํฌ ์ญ์ ์ ์คํจํ์ต๋๋ค.'); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
๐ ์์ ๋ด์ฉ
๐ธ ์คํฌ๋ฆฐ์ท
๐ ๊ธฐํ
Summary by CodeRabbit
์ ๊ธฐ๋ฅ
๋ด๋น๊ฒ์ด์