Api(extension): 익스텐션 api (수정,생성) 연결 및 페이지 구조 수정#80
Conversation
Walkthrough팝업 UI를 DuplicatePop/MainPop의 두 단계로 재구성하고, extension 전용 axios API·React Query 훅·카테고리 훅·타입·리마인드 유틸을 추가했으며 디자인시스템의 DateTime/Dropdown/PopupContainer/iconNames 동작과 tsconfig 경로·팝업 타이틀을 일부 변경했습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User as 사용자
participant App as Extension App
participant Meta as usePageMeta
participant Query as useGetArticleSaved
participant DP as DuplicatePop
participant MP as MainPop
User->>App: 팝업 열기
App->>Meta: 현재 탭 메타 로드 (loading)
App->>Query: URL로 저장 여부 조회
Query-->>App: isSaved(data)
alt isSaved === true
App->>DP: DuplicatePop 렌더
User->>DP: 수정하기 클릭
DP-->>App: onLeftClick (edit 모드)
App->>MP: MainPop(type="edit", savedData)
else
App->>MP: MainPop(type="add")
end
sequenceDiagram
autonumber
actor User as 사용자
participant MP as MainPop
participant API as ReactQuery/API
participant Cat as useCategoryManager
participant BM as useSaveBookmark
Note over MP: 카테고리/메모/리마인드 입력
User->>MP: 저장 클릭
alt type == "add"
MP->>API: postArticle(payload)
API-->>MP: articleId
MP->>BM: saveBookmark(url)
BM-->>MP: 완료
else type == "edit"
MP->>API: putArticle(articleId, payload)
API-->>MP: 완료
end
User->>Cat: 카테고리 생성 요청
Cat->>API: postCategories(title)
API-->>Cat: 생성 결과
Cat-->>MP: 옵션 갱신
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests
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 |
|
✅ Storybook chromatic 배포 확인: |
There was a problem hiding this comment.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/extension/src/hooks/usePageMeta.ts (1)
31-57: OG 메타 요청 실패 시 로딩이 영구히 유지될 수 있습니다. try/finally로 보장하세요.
getOgMeta실패 시setLoading(false)가 호출되지 않아 UI가 멈출 수 있습니다. 아래처럼 예외 처리와 finally를 추가해 주세요.useEffect(() => { chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => { const activeTab = tabs[0]; if (!activeTab?.url) { - setLoading(false); - return; + setLoading(false); + return; } const currentUrl = activeTab.url; chrome.storage.local.set({ bookmarkedUrl: currentUrl }); - const newMeta = await getOgMeta(currentUrl); - // 개발중에는 잠시 주석처리 + try { + const newMeta = await getOgMeta(currentUrl); + setMeta(newMeta); + chrome.storage.local.set({ titleSave: newMeta.title }); + } catch (e) { + // TODO: 로거 연동 또는 사용자 메시지 처리 + console.debug('getOgMeta failed', e); + } finally { + setLoading(false); + } - setMeta(newMeta); - setLoading(false); - - chrome.storage.local.set({ titleSave: newMeta.title }); }); }, []);apps/extension/src/apis/axiosInstance.ts (1)
69-90: 401/403 재시도 로직에서 하드코딩된 이메일과 헤더 설정 방식 문제가 있습니다.
fetchToken('test@gmail.com')하드코딩은 즉각 수정 필요.originalRequest.headers가 존재하지 않을 수 있으니 병합 안전하게 처리.- 동시 401 처리 시 토큰 갱신 폭풍 가능. refresh 락/큐 도입 권장.
-const noAuthNeeded = ['/api/v1/auth/token', '/api/v1/auth/signup']; +const NO_AUTH = ['/api/v1/auth/token', '/api/v1/auth/signup'] as const; ... -const isNoAuth = noAuthNeeded.some((url) => +const isNoAuth = NO_AUTH.some((url) => originalRequest.url?.includes(url) ); ... - originalRequest._retry = true; - const newToken = await fetchToken('test@gmail.com'); - originalRequest.headers.Authorization = `Bearer ${newToken}`; - return apiRequest(originalRequest); + originalRequest._retry = true; + const { email } = await new Promise<{email?: string}>((resolve) => + chrome.storage.local.get(['email'], (res) => resolve(res as any)) + ); + const newToken = await fetchToken(email); + originalRequest.headers = { + ...(originalRequest.headers || {}), + Authorization: `Bearer ${newToken}`, + }; + return apiRequest(originalRequest);참고: 동시 요청 대비용으로 전역
isRefreshing/refreshPromise큐잉을 추가하면 안전합니다.
🧹 Nitpick comments (24)
apps/extension/tsconfig.json (1)
27-27: 중복 path alias 정리 — '@apis/' 유지하고 '@api/' 제거(또는 점진 폐기)하세요.검증 결과 '@apis/'는 코드에서 사용 중이며 '@api/'는 tsconfig에만 존재해 혼선 유발합니다.
- 위치: apps/extension/tsconfig.json (paths에 "@apis/": ["src/apis/"] 및 "@api/": ["src/api/"] 등록 — 파일 내 줄 약 27·30).
- 사용처: apps/extension/src/App.tsx:5, apps/extension/src/pages/MainPop.tsx:16 (둘다 '@apis/query/queries' 참조).
- 조치: tsconfig에서 중복된 "@api/" 항목 제거하거나 주석 처리하여 하나로 통일. 반대로 '@api/'를 표준으로 택하려면 코드베이스의 '@apis/' 사용처를 일괄 치환.
packages/design-system/src/components/dropdown/Dropdown.tsx (2)
26-29: index 계산에 options.indexOf 사용은 중복 옵션에서 오동작합니다. map의 idx를 사용하세요.중복 라벨이 가능하면 최초 인덱스만 반환되어 잘못된 index가 전달됩니다. 또한 key도 중복될 수 있어 재조정 필요합니다.
-const handleSelect = (option: string,idx: number) => { - onChange(option,idx); +const handleSelect = (option: string, idx: number) => { + onChange(option, idx); setIsOpen(false); }; ... -{options.map((option) => ( - <li - key={option} - onClick={() => handleSelect(option,options.indexOf(option))} +{options.map((option, idx) => ( + <li + key={`${option}-${idx}`} + onClick={() => handleSelect(option, idx)} className={`body4-r h-[3.6rem] cursor-pointer p-[0.8rem] ${selectedValue === option ? 'text-main600' : 'text-font-gray-3'}`} > {option} </li> ))}Also applies to: 56-63
26-27: 콤마 뒤 공백 누락.
(option: string,idx: number)→(option: string, idx: number)-const handleSelect = (option: string,idx: number) => { +const handleSelect = (option: string, idx: number) => {apps/extension/src/apis/axiosInstance.ts (4)
42-67: 요청 인터셉터 최적화 및 안정성 제안.
chrome.storage.local.get을 두 번 호출합니다. 키 배열로 한 번에 조회하세요.noAuthNeeded포함 판정은includes대신 정확 매칭 또는startsWith사용을 권장합니다(오탐 방지).-const noAuthNeeded = ['/api/v1/auth/token', '/api/v1/auth/signup']; -const isNoAuth = noAuthNeeded.some((url) => config.url?.includes(url)); +const NO_AUTH = ['/api/v1/auth/token', '/api/v1/auth/signup'] as const; +const isNoAuth = NO_AUTH.some((u) => config.url === u); -if (isNoAuth) return config; +if (isNoAuth) return config; -const email = await new Promise<string | undefined>((resolve) => { - chrome.storage.local.get('email', (result) => { - resolve(result.email); - }); -}); - -let token = await new Promise<string | undefined>((resolve) => { - chrome.storage.local.get('token', (result) => { - resolve(result.token); - }); -}); +const { email, token: storedToken } = await new Promise<{email?: string; token?: string}>((resolve) => { + chrome.storage.local.get(['email','token'], (result) => resolve(result as any)); +}); +let token = storedToken; if (!token || token === 'undefined') { token = await fetchToken(email); } config.headers.Authorization = `Bearer ${token}`; return config;
15-15: console 사용으로 lint 경고 발생. dev 빌드에서만 출력하거나 로거로 대체하세요.-console.log("토큰 저장 완료 ✅"); +if (import.meta.env.DEV) console.debug("토큰 저장 완료 ✅"); ... -console.log('Token re-saved to chrome storage'); +if (import.meta.env.DEV) console.debug('Token re-saved to chrome storage');Also applies to: 37-37
94-99: 타입 네이밍 일관성(PascalCase) 및 필드 의미 명확화 제안.-export interface postSignupRequest { +export interface PostSignupRequest { email: string; - remindDefault: string + remindDefault: string; fcmToken: string; } -export interface postCategoriesRequest { +export interface PostCategoriesRequest { categoryName: string; } -export interface PutArticleRequest { +export interface PutArticleRequest { categoryId: number; memo: string; now: string; remindTime: string | null; }사용처도 함께 치환해 주세요.
Also applies to: 107-111, 123-125, 150-155
3-8: 환경 변수 존재 여부 가드 권장.
VITE_BASE_URL미설정 시 런타임 오류가 납니다. 초기화 시 점검하거나 에러 메시지를 명확히 해주세요.-const apiRequest = axios.create({ - baseURL: import.meta.env.VITE_BASE_URL, +const baseURL = import.meta.env.VITE_BASE_URL; +if (!baseURL) { + throw new Error('VITE_BASE_URL is not defined for extension API client'); +} +const apiRequest = axios.create({ + baseURL, headers: { 'Content-Type': 'application/json' }, });packages/design-system/src/components/popup/PopupContainer.tsx (2)
26-31: 반환 타입/캐스팅 정리로 타입 일관성 개선함수 시그니처는 React.ReactNode를 명시하고, 반환 시에는
as unknown as React.ReactElement로 캐스팅하고 있어 불필요한 불일치가 있습니다. 타입 추론에 맡기거나 명시적으로React.ReactPortal | null을 반환하도록 정리하세요.아래처럼 간단히 정리 가능합니다:
-}: PopupContainerProps): React.ReactNode => { +}: PopupContainerProps => { ... - ) as unknown as React.ReactElement; + );Also applies to: 55-56
41-47: SSR/비-DOM 환경 안전 가드 및 접근성 속성 추가 제안design-system이 SSR 환경에서도 재사용될 수 있다면
document존재 여부를 가드하고, 모달 의미를 명확히 하기 위해 접근성 속성도 권장합니다.if (!isOpen) return null; + if (typeof document === 'undefined') return null; return createPortal( - <div className="fixed inset-0 z-10 flex items-center justify-center"> + <div className="fixed inset-0 z-10 flex items-center justify-center" role="dialog" aria-modal="true">apps/extension/src/pages/DuplicatePop.tsx (1)
9-11: 버튼 기본 타입 지정 및 과도한 포인터 커서 제거
- wrapper에
cursor-pointer가 있어 전체 카드가 클릭 가능한 것처럼 보입니다. 실제 액션은 버튼에만 있으므로 혼동 방지를 위해 제거하세요.- 버튼은 폼 내부에 렌더링될 가능성에 대비해
type="button"을 명시하세요.- <div className="bg-white-bg flex w-[26rem] cursor-pointer flex-col items-center justify-center rounded-[1.2rem] px-[1.6rem] py-[2.4rem] shadow-[0_0_32px_0_rgba(0,0,0,0.10)]"> + <div className="bg-white-bg flex w-[26rem] flex-col items-center justify-center rounded-[1.2rem] px-[1.6rem] py-[2.4rem] shadow-[0_0_32px_0_rgba(0,0,0,0.10)]"> ... - <button + <button + type="button" className="border-gray200 sub5-sb bg-white-bg text-font-black-1 w-[10.8rem] rounded-[0.4rem] border py-[0.85rem]" onClick={onLeftClick} > 수정하기 </button> - <button + <button + type="button" className="sub5-sb bg-gray900 text-white-bg w-[10.8rem] rounded-[0.4rem] py-[0.85rem]" onClick={onRightClick} > 대시보드 </button>Also applies to: 13-24
apps/extension/src/apis/query/queries.ts (5)
39-44: 읽기 전용 데이터는 캐싱 파라미터 지정카테고리는 변동 빈도가 낮으므로 낭비되는 네트워크를 줄이기 위해
staleTime을 부여하세요.export const useGetCategoriesExtension = () => { return useQuery({ queryKey: ["categoriesExtension"], queryFn: getCategoriesExtension, + staleTime: 5 * 60 * 1000, // 5분 }); };
46-51: 리마인드 타임도 캐싱/포커스 리패치 제어팝업 전환 시 불필요한 재요청 방지를 위해 옵션 추가를 권장합니다.
export const useGetRemindTime = () => { return useQuery({ queryKey: ["remindTime"], queryFn: getRemindTime, + refetchOnWindowFocus: false, + staleTime: 60 * 1000, }); }
61-72: 아티클 수정 후 연관 캐시 무효화 훅 시그니처 보강수정 후 중복 여부/리스트 동기화를 위해 URL 키를 받아 무효화하거나, 옵션으로
onSuccess를 받아 외부에서 제어 가능하도록 확장하세요.-export const usePutArticle = () => { - return useMutation({ - mutationFn: ({ articleId, data }: { articleId: number; data: PutArticleRequest }) => - putArticle(articleId, data), +export const usePutArticle = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ articleId, data, url }: { articleId: number; data: PutArticleRequest; url?: string }) => + putArticle(articleId, data), onSuccess: (data) => { - console.log("아티클 수정 성공:", data); + if (data?.url) { + queryClient.invalidateQueries({ queryKey: ["articleSaved", data.url] }); + } + // 호출 측에서 url을 넘긴 경우에도 보수적으로 무효화 + // (백엔드 응답에 url이 없을 때 대비) + // queryClient.invalidateQueries({ queryKey: ["articleSaved", url] }); + if (process.env.NODE_ENV !== "production") { + // eslint-disable-next-line no-console + console.log("아티클 수정 성공:", data); + } }, onError: (error) => { - console.error("아티클 수정 실패:", error); + if (process.env.NODE_ENV !== "production") { + // eslint-disable-next-line no-console + console.error("아티클 수정 실패:", error); + } }, }); };
16-26: 콘솔 로깅은 개발 환경에서만 — 린트 경고 해결현재 파일 전반의
console.*로 인해 lint 경고가 발생합니다. 프로덕션에서는 억제하세요. design-system의 토스트/알럿으로 대체하거나 환경 가드로 제한하세요.예시(패턴만 제시):
- console.log("회원가입 성공:", data); + if (process.env.NODE_ENV !== "production") { + // eslint-disable-next-line no-console + console.log("회원가입 성공:", data); + }
2-2: 타입 명명 컨벤션
postSignupRequest인터페이스는 소문자로 시작합니다. 프로젝트 컨벤션에 맞춰PostSignupRequest로 정렬하는 것을 권장합니다. (동일 파일/호출부 일괄 변경 필요)apps/extension/src/App.tsx (2)
9-11: URL 로딩 완료 후에만 중복 조회 실행초기
url이 빈 문자열이라도 훅 내부에서 enabled로 보호하고 있지만, 로딩 상태를 반영하면 명확합니다.- const { url } = usePageMeta(); - const { data: isSaved } = useGetArticleSaved(url); + const { url, loading } = usePageMeta(); + const normalizedUrl = loading ? "" : url; + const { data: isSaved } = useGetArticleSaved(normalizedUrl);
30-39: 로딩/에러 UI 고려(선택)팝업 진입 시 빈 화면 느낌을 줄이려면
usePageMeta/useGetArticleSaved의 로딩/에러 상태를 반영하는 경량 UI를 추가하는 것을 권장합니다. (스피너/문구 정도)apps/extension/src/pages/MainPop.tsx (7)
45-49: 변수명 isArticleId는 boolean처럼 읽힙니다 — articleId로 변경 권장의미와 타입에 맞게 명명하면 가독성이 좋아집니다.
- const [isArticleId, setIsArticleId] = useState(0); + const [articleId, setArticleId] = useState<number>(0); ```<!-- review_comment_end --> --- `71-72`: **위 변수명 변경에 따른 사용처 수정** 누락 방지를 위해 함께 변경하세요. ```diff - setIsArticleId(savedData.id ?? 0); + setArticleId(savedData.id ?? 0); ... - articleId: isArticleId, + articleId: articleId, ```<!-- review_comment_end --> Also applies to: 208-209 --- `56-61`: **카테고리 ID는 number로 관리해 불필요한 parseInt를 제거하세요** 상태를 number로 두면 변환 오버헤드/에러를 줄일 수 있습니다. ```diff -const [selected, setSelected] = useState<string | null>( +const [selected, setSelected] = useState<number | null>( type === "edit" && savedData?.categoryResponse?.categoryId - ? savedData?.categoryResponse?.categoryId.toString() + ? savedData?.categoryResponse?.categoryId : null );- const handleSelect = (value: string | null, idx: number) => { - const categoryId = categoryData?.data?.categories[idx]?.categoryId.toString() ?? null; + const handleSelect = (value: string | null, idx: number) => { + const categoryId = categoryData?.data?.categories[idx]?.categoryId ?? null; setSelected(categoryId); setSelectedCategoryName(value); };- categoryId: saveData.selectedCategory - ? parseInt(saveData.selectedCategory) - : 0, + categoryId: saveData.selectedCategory ?? 0,그리고 선택 검증도 단순화 가능합니다:
- if (!selected || parseInt(selected) === 0) { + if (selected == null) { ```<!-- review_comment_end --> Also applies to: 236-240, 197-204 --- `149-156`: **외부 이미지 핫링크 대신 번들된 기본 이미지를 사용하세요** 외부 URL 가용성/라이선스/성능 문제가 있습니다. 확장 내 자산(chrome.runtime.getURL)로 대체를 권장합니다. 예: setImgUrl(chrome.runtime.getURL('assets/fallback-thumbnail.png')) (자산 경로 확인 필요).<!-- review_comment_end --> --- `139-148`: **메타 로딩 실패 시 저장 차단/안내 추가 권장** 현재 알림/창닫기 로직이 주석 처리되어 있어 url/title이 비어도 저장을 시도할 수 있습니다. handleSave 초두에서 loading/url 검사로 얼리 리턴하거나 버튼 disabled 조건에 반영하세요(상단 제안과 연동).<!-- review_comment_end --> --- `36-37`: **디버그 로그 제거** 불필요한 콘솔 출력은 남기지 않는 것이 좋습니다. ```diff -const MainPop = ({type, savedData}: MainPopProps) => { - console.log(savedData) +const MainPop = ({type, savedData}: MainPopProps) => {
164-165: 기본 카테고리 "안 읽은 정보" 존재/순서 보장 확인팀 룰에 따라 첫 번째 기본 카테고리로 고정되어야 합니다. API가 항상 포함해서 내려주는지, 누락 시 클라이언트가 보정해야 하는지 결정이 필요합니다.
필요 시 options 구성 시 최상단으로 보정하는 가드 추가를 도와드릴 수 있습니다(중복 방지 포함).
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (4)
packages/design-system/src/icons/source/chippi_profile.svgis excluded by!**/*.svgpackages/design-system/src/icons/source/dotori.svgis excluded by!**/*.svgpackages/design-system/src/icons/source/logo.svgis excluded by!**/*.svgpackages/design-system/src/icons/source/saved.svgis excluded by!**/*.svg
📒 Files selected for processing (10)
apps/extension/src/App.tsx(1 hunks)apps/extension/src/apis/axiosInstance.ts(2 hunks)apps/extension/src/apis/query/queries.ts(1 hunks)apps/extension/src/hooks/usePageMeta.ts(3 hunks)apps/extension/src/pages/DuplicatePop.tsx(1 hunks)apps/extension/src/pages/MainPop.tsx(1 hunks)apps/extension/tsconfig.json(1 hunks)packages/design-system/src/components/dropdown/Dropdown.tsx(3 hunks)packages/design-system/src/components/popup/PopupContainer.tsx(1 hunks)packages/design-system/src/icons/iconNames.ts(2 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-07-17T09:18:13.818Z
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#102
File: apps/extension/src/components/modalPop/ModalPop.tsx:166-172
Timestamp: 2025-07-17T09:18:13.818Z
Learning: In apps/extension/src/components/modalPop/ModalPop.tsx, the categories array should include "안 읽은 정보" (Unread Information) as the first default category that cannot be deleted. This default category is used consistently across the client-side dashboard and should be protected from deletion in the extension as well.
Applied to files:
apps/extension/src/pages/MainPop.tsx
📚 Learning: 2025-07-08T11:47:10.642Z
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/App.tsx:10-21
Timestamp: 2025-07-08T11:47:10.642Z
Learning: In apps/extension/src/App.tsx, the InfoBox component currently uses a hardcoded external URL for the icon prop as a temporary static placeholder. The plan is to replace this with dynamic favicon extraction from bookmarked websites in future iterations.
Applied to files:
apps/extension/src/App.tsx
📚 Learning: 2025-09-11T11:48:10.615Z
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#75
File: apps/extension/src/apis/axiosInstance.ts:30-34
Timestamp: 2025-09-11T11:48:10.615Z
Learning: Pinback 프로젝트에서는 사용자 이메일 저장 시 'email' 키를 사용하도록 통일했습니다 (localStorage 및 chrome.storage.local 모두).
Applied to files:
apps/extension/src/apis/axiosInstance.ts
🧬 Code graph analysis (3)
apps/extension/src/apis/query/queries.ts (1)
apps/extension/src/apis/axiosInstance.ts (11)
PostArticleRequest(94-99)postArticle(101-104)postSignupRequest(107-111)postSignup(113-116)postCategoriesRequest(123-125)postCategories(127-130)getCategoriesExtension(118-121)getRemindTime(132-140)getArticleSaved(143-148)PutArticleRequest(150-155)putArticle(157-160)
apps/extension/src/pages/MainPop.tsx (5)
apps/extension/src/apis/axiosInstance.ts (3)
postArticle(101-104)postCategories(127-130)putArticle(157-160)apps/extension/src/apis/query/queries.ts (5)
usePostArticle(4-14)usePostCategories(28-38)usePutArticle(61-72)useGetCategoriesExtension(39-44)useGetRemindTime(46-51)apps/extension/src/hooks/usePageMeta.ts (1)
usePageMeta(21-61)apps/extension/src/hooks/useSaveBookmarks.ts (1)
useSaveBookmark(13-50)packages/design-system/src/components/dateTime/DateTime.tsx (1)
DateTime(56-176)
apps/extension/src/App.tsx (2)
apps/extension/src/hooks/usePageMeta.ts (1)
usePageMeta(21-61)apps/extension/src/apis/query/queries.ts (1)
useGetArticleSaved(53-59)
🪛 GitHub Check: lint
apps/extension/src/apis/query/queries.ts
[warning] 66-66:
Unexpected console statement
[warning] 35-35:
Unexpected console statement
[warning] 32-32:
Unexpected console statement
[warning] 23-23:
Unexpected console statement
[warning] 20-20:
Unexpected console statement
[warning] 11-11:
Unexpected console statement
[warning] 8-8:
Unexpected console statement
apps/extension/src/apis/axiosInstance.ts
[warning] 15-15:
Unexpected console statement
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: storybook
🔇 Additional comments (11)
packages/design-system/src/icons/iconNames.ts (3)
4-4: 아이콘 'dotori' 추가 LGTM.
17-18: 'main_logo' 재배치 및 'saved' 추가 LGTM.
1-24: 자동 생성 파일 수동 변경 여부 확인 필요.이 파일은 “자동 생성(직접 수정 금지)” 표기입니다. 생성 파이프라인/소스(svg 목록 등)도 함께 갱신되었는지 확인해 주세요. 수동 변경이면 다음 빌드에서 덮어써질 수 있습니다.
apps/extension/src/hooks/usePageMeta.ts (1)
60-60: 훅 반환 시그니처 변경 — 검증 완료
- 변경: usePageMeta가 return { ...meta, loading }을 반환하도록 수정된 것을 확인했습니다.
- 호출 위치(레포 검색 결과):
- apps/extension/src/pages/MainPop.tsx — const { url, title, description, imgUrl: initialImgUrl ,loading } = usePageMeta(); (loading 사용)
- apps/extension/src/App.tsx — const { url } = usePageMeta(); (loading 미사용)
- 다른 호출이나 .loading 접근 없음 → 현재로서는 추가 수정 불필요.
apps/extension/src/apis/axiosInstance.ts (3)
132-137: now 파라미터의 타임존 정보가 손실됩니다.
toISOString().split(".")[0]는 밀리초와 'Z'를 제거합니다. 서버가 UTC를 기대한다면 전체 ISO(예:2025-09-11T12:34:56.789Z)를 넘기거나, 서버 스펙에 맞게 명시적으로 포맷을 맞춰 주세요.-const now = new Date().toISOString().split(".")[0]; +const now = new Date().toISOString(); // 서버가 UTC ISO를 기대한다면 이 값을 사용
48-52: 스토리지 키 ‘email’ 사용 일관성 좋습니다.
101-121: 엔드포인트 래퍼들의 형태/반환값 일관성 좋습니다.Also applies to: 127-131, 133-141, 143-149, 157-160
packages/design-system/src/components/popup/PopupContainer.tsx (1)
49-49: 기본 close 핸들러로의 폴백 처리 좋습니다.
onLeftClick={popupProps.onLeftClick ?? onClose}로 커스터마이즈 가능하면서 안전한 기본 동작을 확보했습니다.apps/extension/src/App.tsx (1)
26-28: 팝업에서 새 탭으로 대시보드 열기(chrome.tabs.create 사용)파일: apps/extension/src/App.tsx (라인 26-28)
팝업 내부에서 window.location.href로 이동하면 새 탭이 열리지 않고 팝업만 변경될 수 있으니 chrome.tabs.create로 새 탭을 열고 window.close()로 팝업을 닫으세요. URL은 절대경로로 설정하세요.
- const handleDuplicateRightClick = () => { - window.location.href = "/dashboard"; - }; + const handleDuplicateRightClick = () => { + const DASHBOARD_URL = "https://pinback.app/dashboard"; // 실제 서비스 URL로 교체 + if (typeof chrome !== "undefined" && chrome?.tabs?.create) { + chrome.tabs.create({ url: DASHBOARD_URL }); + window.close(); + } else { + window.location.href = DASHBOARD_URL; + } + };대시보드 실제 URL을 확인해 DASHBOARD_URL 상수를 교체하세요. 포맷: https://<서비스도메인>/dashboard
apps/extension/src/pages/MainPop.tsx (2)
86-106: 오전/오후(12시간) → 24시간 변환 추가 필요 (API 포맷 불일치 방지)UI가 "오전/오후 hh:mm"을 표시하면 저장 전에 24시간(HH:mm)으로 변환하고, 백엔드의 타임존 요구(Z/±HH:MM)를 확인하세요.
// HH:mm:ss → HH:mm const updateTime = (time: string) => { if (!time) return ""; return time.slice(0, 5); }; + // "오전 hh:mm"/"오후 hh:mm" → "HH:mm" (이미 24h이면 그대로 반환) + const to24h = (t: string) => { + const m = t.match(/^(오전|오후)\s+(\d{1,2}):(\d{2})$/); + if (!m) return t; + const [, meridiem, hh, mm] = m; + let H = parseInt(hh, 10) % 12; + if (meridiem === "오후") H += 12; + return `${String(H).padStart(2, "0")}:${mm}`; + }; const combineDateTime = (date: string, time: string) => { if (!date || !time) return null; const formattedDate = date.replace(/\./g, "-"); - const formattedTime = time.length === 5 ? `${time}:00` : time; + const hhmm = to24h(time); + const formattedTime = hhmm.length === 5 ? `${hhmm}:00` : hhmm; return `${formattedDate}T${formattedTime}`; };
206-219: now 필드는 PutArticleRequest에 정의되어 있어 제거 불필요apps/extension/src/apis/axiosInstance.ts:150–155의 export interface PutArticleRequest에 now: string이 선언되어 있으므로 MainPop.tsx의 now: new Date().toISOString()는 타입과 일치합니다. 제안된 diff는 적용할 필요 없음.
Likely an incorrect or invalid review comment.
| import { useMutation,useQuery } from "@tanstack/react-query"; | ||
| import { postArticle, PostArticleRequest,postSignup, postSignupRequest, getCategoriesExtension, postCategories, postCategoriesRequest, getRemindTime, getArticleSaved,putArticle, PutArticleRequest} from "../axiosInstance"; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
queryClient를 임포트하여 성공 시 캐시 무효화가 가능하도록 준비
후속 코멘트들에서 제안하는 invalidate를 위해 useQueryClient를 함께 임포트하세요.
-import { useMutation,useQuery } from "@tanstack/react-query";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";📝 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.
| import { useMutation,useQuery } from "@tanstack/react-query"; | |
| import { postArticle, PostArticleRequest,postSignup, postSignupRequest, getCategoriesExtension, postCategories, postCategoriesRequest, getRemindTime, getArticleSaved,putArticle, PutArticleRequest} from "../axiosInstance"; | |
| import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; | |
| import { postArticle, PostArticleRequest,postSignup, postSignupRequest, getCategoriesExtension, postCategories, postCategoriesRequest, getRemindTime, getArticleSaved,putArticle, PutArticleRequest} from "../axiosInstance"; |
🤖 Prompt for AI Agents
In apps/extension/src/apis/query/queries.ts around lines 1 to 2, the file
imports useMutation and useQuery but not useQueryClient; import useQueryClient
from "@tanstack/react-query" alongside the existing imports so you can call
useQueryClient() in hooks and perform cache invalidation (invalidateQueries) on
successful mutations as suggested by later comments.
| export const usePostArticle = () => { | ||
| return useMutation({ | ||
| mutationFn: (data: PostArticleRequest) => postArticle(data), | ||
| onSuccess: (data) => { | ||
| console.log("저장 성공:", data); | ||
| }, | ||
| onError: (error) => { | ||
| console.error("저장 실패:", error); | ||
| }, | ||
| }); | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
아티클 저장 성공 시 관련 캐시 무효화 (UI 동기화 필수)
저장 후 중복 여부/상태를 사용하는 화면과 동기화를 위해 ["articleSaved", data.url] 키를 무효화하세요.
export const usePostArticle = () => {
- return useMutation({
+ const queryClient = useQueryClient();
+ return useMutation({
mutationFn: (data: PostArticleRequest) => postArticle(data),
onSuccess: (data) => {
- console.log("저장 성공:", data);
+ // 중복 여부 재조회
+ if (data?.url) {
+ queryClient.invalidateQueries({ queryKey: ["articleSaved", data.url] });
+ }
+ if (process.env.NODE_ENV !== "production") {
+ // eslint-disable-next-line no-console
+ console.log("저장 성공:", data);
+ }
},
onError: (error) => {
- console.error("저장 실패:", error);
+ if (process.env.NODE_ENV !== "production") {
+ // eslint-disable-next-line no-console
+ console.error("저장 실패:", error);
+ }
},
});
};📝 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.
| export const usePostArticle = () => { | |
| return useMutation({ | |
| mutationFn: (data: PostArticleRequest) => postArticle(data), | |
| onSuccess: (data) => { | |
| console.log("저장 성공:", data); | |
| }, | |
| onError: (error) => { | |
| console.error("저장 실패:", error); | |
| }, | |
| }); | |
| }; | |
| export const usePostArticle = () => { | |
| const queryClient = useQueryClient(); | |
| return useMutation({ | |
| mutationFn: (data: PostArticleRequest) => postArticle(data), | |
| onSuccess: (data) => { | |
| // 중복 여부 재조회 | |
| if (data?.url) { | |
| queryClient.invalidateQueries({ queryKey: ["articleSaved", data.url] }); | |
| } | |
| if (process.env.NODE_ENV !== "production") { | |
| // eslint-disable-next-line no-console | |
| console.log("저장 성공:", data); | |
| } | |
| }, | |
| onError: (error) => { | |
| if (process.env.NODE_ENV !== "production") { | |
| // eslint-disable-next-line no-console | |
| console.error("저장 실패:", error); | |
| } | |
| }, | |
| }); | |
| }; |
🧰 Tools
🪛 GitHub Check: lint
[warning] 11-11:
Unexpected console statement
[warning] 8-8:
Unexpected console statement
🤖 Prompt for AI Agents
In apps/extension/src/apis/query/queries.ts around lines 4 to 14, the onSuccess
handler for usePostArticle currently only logs success and does not invalidate
related cache; update onSuccess to call the query client to invalidate the
["articleSaved", data.url] key so UI using that key is refreshed after save
(e.g., obtain the queryClient via useQueryClient() and call
queryClient.invalidateQueries(["articleSaved", data.url]) inside onSuccess).
| export const usePostCategories = () => { | ||
| return useMutation({ | ||
| mutationFn: (data: postCategoriesRequest) => postCategories(data), | ||
| onSuccess: (data) => { | ||
| console.log("카테고리 저장", data); | ||
| }, | ||
| onError: (error) => { | ||
| console.error("카테고리 저장 실패", error); | ||
| }, | ||
| }); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
카테고리 생성 성공 시 목록 캐시 무효화
생성 후 ["categoriesExtension"]을 무효화해 드롭다운/목록을 즉시 최신화하세요.
export const usePostCategories = () => {
- return useMutation({
+ const queryClient = useQueryClient();
+ return useMutation({
mutationFn: (data: postCategoriesRequest) => postCategories(data),
onSuccess: (data) => {
- console.log("카테고리 저장", data);
+ queryClient.invalidateQueries({ queryKey: ["categoriesExtension"] });
+ if (process.env.NODE_ENV !== "production") {
+ // eslint-disable-next-line no-console
+ console.log("카테고리 저장", data);
+ }
},
onError: (error) => {
- console.error("카테고리 저장 실패", error);
+ if (process.env.NODE_ENV !== "production") {
+ // eslint-disable-next-line no-console
+ console.error("카테고리 저장 실패", error);
+ }
},
});
}📝 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.
| export const usePostCategories = () => { | |
| return useMutation({ | |
| mutationFn: (data: postCategoriesRequest) => postCategories(data), | |
| onSuccess: (data) => { | |
| console.log("카테고리 저장", data); | |
| }, | |
| onError: (error) => { | |
| console.error("카테고리 저장 실패", error); | |
| }, | |
| }); | |
| } | |
| export const usePostCategories = () => { | |
| const queryClient = useQueryClient(); | |
| return useMutation({ | |
| mutationFn: (data: postCategoriesRequest) => postCategories(data), | |
| onSuccess: (data) => { | |
| queryClient.invalidateQueries({ queryKey: ["categoriesExtension"] }); | |
| if (process.env.NODE_ENV !== "production") { | |
| // eslint-disable-next-line no-console | |
| console.log("카테고리 저장", data); | |
| } | |
| }, | |
| onError: (error) => { | |
| if (process.env.NODE_ENV !== "production") { | |
| // eslint-disable-next-line no-console | |
| console.error("카테고리 저장 실패", error); | |
| } | |
| }, | |
| }); | |
| } |
🧰 Tools
🪛 GitHub Check: lint
[warning] 35-35:
Unexpected console statement
[warning] 32-32:
Unexpected console statement
🤖 Prompt for AI Agents
In apps/extension/src/apis/query/queries.ts around lines 28 to 38, the onSuccess
handler for usePostCategories currently only logs success and does not update
cached category lists; after a successful post, call useQueryClient() at the top
of the hook to get queryClient and then invoke
queryClient.invalidateQueries(["categoriesExtension"]) inside onSuccess (after
any logging) so the dropdown/list query is immediately refreshed; ensure
useQueryClient is imported from react-query (or @tanstack/react-query) if not
already.
| export const useGetArticleSaved = (url:string) => { | ||
| return useQuery({ | ||
| queryKey: ["articleSaved", url], | ||
| queryFn: () => getArticleSaved(url), | ||
| enabled: !!url, | ||
| }); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
URL 검증 및 리패치로 인한 깜빡임 방지
chrome://,edge://,about:같은 내부 페이지와http(s)가 아닌 스킴은 호출을 막으세요.- 포커스 리패치로
DuplicatePop이 다시 뜨는 현상을 줄이기 위해 옵션을 지정하세요.
export const useGetArticleSaved = (url:string) => {
+ const isHttpUrl = /^https?:\/\//i.test(url);
return useQuery({
queryKey: ["articleSaved", url],
- queryFn: () => getArticleSaved(url),
- enabled: !!url,
+ queryFn: () => getArticleSaved(url),
+ enabled: !!url && isHttpUrl,
+ refetchOnWindowFocus: false,
+ staleTime: 30 * 1000,
});
}📝 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.
| export const useGetArticleSaved = (url:string) => { | |
| return useQuery({ | |
| queryKey: ["articleSaved", url], | |
| queryFn: () => getArticleSaved(url), | |
| enabled: !!url, | |
| }); | |
| } | |
| export const useGetArticleSaved = (url:string) => { | |
| const isHttpUrl = /^https?:\/\//i.test(url); | |
| return useQuery({ | |
| queryKey: ["articleSaved", url], | |
| queryFn: () => getArticleSaved(url), | |
| enabled: !!url && isHttpUrl, | |
| refetchOnWindowFocus: false, | |
| staleTime: 30 * 1000, | |
| }); | |
| } |
🤖 Prompt for AI Agents
In apps/extension/src/apis/query/queries.ts around lines 53 to 59, validate the
incoming url and prevent queries for internal/non-http schemes and disable
focus-triggered refetching: only enable the query when url is a well-formed
HTTP/HTTPS URL (parse with the URL constructor or a small regex and reject
schemes like chrome:, edge:, about:, file:, data:, etc.), and add react-query
options to reduce refetch flicker such as refetchOnWindowFocus: false and
refetchOnMount: false (and optionally refetchOnReconnect: false) so the hook
doesn't re-run on focus and cause DuplicatePop to reappear.
apps/extension/src/pages/MainPop.tsx
Outdated
| if (type === "add"){ | ||
| save({ | ||
| url, | ||
| title, | ||
| description, | ||
| imgUrl, | ||
| memo, | ||
| isRemindOn, | ||
| selectedCategory: selected, | ||
| date: isRemindOn ? date : null, | ||
| time: isRemindOn ? time : null, | ||
| }); | ||
| postArticle( | ||
| { | ||
| url, | ||
| categoryId: saveData.selectedCategory | ||
| ? parseInt(saveData.selectedCategory) | ||
| : 0, | ||
| memo: saveData.memo, | ||
| remindTime: combineDateTime(saveData.date ?? "", saveData.time ?? ""), | ||
| } | ||
| ); | ||
| } else{ |
There was a problem hiding this comment.
🛠️ Refactor suggestion
add 플로우에서 비동기 호출 대기/에러 처리 누락
저장 로컬(save)과 서버(postArticle)를 동시에 발사하고 에러를 삼킵니다. 사용자 경험과 일관성을 위해 await + try/catch로 제어하세요.
- if (type === "add"){
- save({ ... });
- postArticle(
- {
- url,
- categoryId: saveData.selectedCategory
- ? parseInt(saveData.selectedCategory)
- : 0,
- memo: saveData.memo,
- remindTime: combineDateTime(saveData.date ?? "", saveData.time ?? ""),
- }
- );
- } else{
+ if (type === "add"){
+ try {
+ setSaving(true);
+ // 서버 저장 먼저 시도(선호). 필요 시 성공 후 로컬 보관.
+ await postArticle({
+ url,
+ categoryId: saveData.selectedCategory
+ ? parseInt(saveData.selectedCategory)
+ : 0,
+ memo: saveData.memo,
+ remindTime: combineDateTime(saveData.date ?? "", saveData.time ?? ""),
+ });
+ await save({ ...saveData });
+ // TODO: 성공 토스트/창 닫기 등 UX 처리
+ } catch (e) {
+ alert("저장에 실패했습니다. 잠시 후 다시 시도해주세요.");
+ } finally {
+ setSaving(false);
+ }
+ } else{로컬 우선이 필요하면 순서만 바꾸고, 실패 시 롤백/표시를 고려하세요.
📝 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.
| if (type === "add"){ | |
| save({ | |
| url, | |
| title, | |
| description, | |
| imgUrl, | |
| memo, | |
| isRemindOn, | |
| selectedCategory: selected, | |
| date: isRemindOn ? date : null, | |
| time: isRemindOn ? time : null, | |
| }); | |
| postArticle( | |
| { | |
| url, | |
| categoryId: saveData.selectedCategory | |
| ? parseInt(saveData.selectedCategory) | |
| : 0, | |
| memo: saveData.memo, | |
| remindTime: combineDateTime(saveData.date ?? "", saveData.time ?? ""), | |
| } | |
| ); | |
| } else{ | |
| if (type === "add"){ | |
| try { | |
| setSaving(true); | |
| // 서버 저장 먼저 시도(선호). 필요 시 성공 후 로컬 보관. | |
| await postArticle({ | |
| url, | |
| categoryId: saveData.selectedCategory | |
| ? parseInt(saveData.selectedCategory) | |
| : 0, | |
| memo: saveData.memo, | |
| remindTime: combineDateTime(saveData.date ?? "", saveData.time ?? ""), | |
| }); | |
| await save({ ...saveData }); | |
| // TODO: 성공 토스트/창 닫기 등 UX 처리 | |
| } catch (e) { | |
| alert("저장에 실패했습니다. 잠시 후 다시 시도해주세요."); | |
| } finally { | |
| setSaving(false); | |
| } | |
| } else{ |
🤖 Prompt for AI Agents
In apps/extension/src/pages/MainPop.tsx around lines 184 to 206, the add flow
fires save (local) and postArticle (server) concurrently and swallows errors;
wrap the server call in a try/catch and await it to ensure errors are handled
and user is informed. Change the flow to perform local save first (or
server-first if preferred), then await postArticle inside a try/catch; on server
error, show an error message and either roll back the local save or mark the
item as unsynced/pending so the UI reflects failure; ensure parseInt logic and
combineDateTime inputs are preserved when calling postArticle.
apps/extension/src/pages/MainPop.tsx
Outdated
| const [categoryTitle, setCategoryTitle] = useState(''); | ||
| const [isPopError, setIsPopError] = useState(false); | ||
| const [errorTxt, setErrorTxt] = useState(''); | ||
| const saveCategory = () => { | ||
| // 20자 제한 | ||
| if (categoryTitle.length >20){ | ||
| setIsPopError(true); | ||
| setErrorTxt('20자 이내로 작성해주세요'); | ||
| } else{ | ||
| postCategories({categoryName:categoryTitle}); | ||
| setIsPopupOpen(false); | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
카테고리 추가: 공백/중복/예약어(안 읽은 정보) 차단 및 목록 갱신 누락
현재는 20자 제한만 있으며 저장 후 카테고리 리스트 갱신이 없습니다. UX/데이터 정합성 보완이 필요합니다.
+import { useQueryClient } from "@tanstack/react-query";
...
- const [categoryTitle, setCategoryTitle] = useState('');
+ const [categoryTitle, setCategoryTitle] = useState('');
const [isPopError, setIsPopError] = useState(false);
const [errorTxt, setErrorTxt] = useState('');
- const saveCategory = () => {
+ const queryClient = useQueryClient();
+ const saveCategory = async () => {
// 20자 제한
- if (categoryTitle.length >20){
+ const title = categoryTitle.trim();
+ if (title.length === 0) {
+ setIsPopError(true);
+ setErrorTxt('한 글자 이상 입력해주세요');
+ return;
+ }
+ if (title.length > 20){
setIsPopError(true);
setErrorTxt('20자 이내로 작성해주세요');
- } else{
- postCategories({categoryName:categoryTitle});
- setIsPopupOpen(false);
- }
+ return;
+ }
+ // 중복/예약어 차단
+ const names = (categoryData?.data?.categories ?? []).map((c: any) => c.categoryName);
+ if (names.includes(title)) {
+ setIsPopError(true);
+ setErrorTxt('이미 존재하는 카테고리입니다');
+ return;
+ }
+ if (title === '안 읽은 정보') {
+ setIsPopError(true);
+ setErrorTxt('기본 카테고리 이름은 사용할 수 없어요');
+ return;
+ }
+ try {
+ await postCategories({ categoryName: title });
+ await queryClient.invalidateQueries({ queryKey: ['categoriesExtension'] });
+ setIsPopupOpen(false);
+ setCategoryTitle('');
+ setIsPopError(false);
+ setErrorTxt('');
+ } catch (e) {
+ setIsPopError(true);
+ setErrorTxt('카테고리 저장에 실패했습니다');
+ }
}- onAddItem={() => setIsPopupOpen(true)}
+ onAddItem={() => {
+ setCategoryTitle('');
+ setIsPopError(false);
+ setErrorTxt('');
+ setIsPopupOpen(true);
+ }}또한 기본 카테고리 "안 읽은 정보"가 항상 첫 번째로 존재해야 한다는 팀 규칙(7/17 학습 정보)을 서버/클라이언트 한쪽에서 보장하는지 확인 바랍니다.
Also applies to: 283-285
🤖 Prompt for AI Agents
In apps/extension/src/pages/MainPop.tsx around lines 223-235 (also apply same
fixes at lines 283-285), the saveCategory flow only enforces a 20-char limit and
does not refresh the category list or guard against empty, duplicate, or
reserved-name entries; update saveCategory to trim input and reject empty titles
(set isPopError and errorTxt), check existing categories (passed via
props/state) and reject duplicates with an error, explicitly block the reserved
name "안 읽은 정보" (or normalize comparisons) and show a clear error, call the API
to create the category and after a successful response refresh the category list
(e.g., re-fetch or update local state) ensuring the reserved "안 읽은 정보" is always
present as the first item (if missing, insert/prepend it on client or ensure
server returns it), and close the popup only after the create and list-refresh
succeed; keep error handling for API failures by setting isPopError and
errorTxt.
| options: string[]; | ||
| selectedValue: string | null; | ||
| onChange: (selected: string | null) => void; | ||
| onChange: (selected: string | null, index: number) => void; |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
onChange 시그니처 변경에 따른 하위 호환성 점검.
디자인 시스템 컴포넌트의 퍼블릭 API가 바뀌었으므로 모든 호출부 업데이트/버전 범프가 필요합니다.
🏁 Script executed:
#!/bin/bash
# Dropdown 사용처의 onChange 핸들러 점검
rg -nP --type=ts --type=tsx '<Dropdown\b' -C3Length of output: 27
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "레포 루트: $(pwd)"
echo "검색: '<Dropdown' 사용처 찾기..."
rg -n --hidden -S "<Dropdown\b" --glob '**/*.{ts,tsx,js,jsx}' | cut -d: -f1 | sort -u > /tmp/dropdown_files.txt || true
if [ ! -s /tmp/dropdown_files.txt ]; then
echo "찾은 파일 없음."
exit 0
fi
echo "찾은 파일 목록:"
cat /tmp/dropdown_files.txt
echo ""
echo "'onChange' prop 발생 위치(컨텍스트 포함):"
while IFS= read -r f; do
echo "---- $f ----"
rg -n --hidden -S -C3 "onChange\s*=" "$f" || echo " (onChange prop 없음)"
done < /tmp/dropdown_files.txtLength of output: 2658
onChange 시그니처 변경 — 호출부 수정 및 패키지 버전 범프 필요
디자인 시스템 Dropdown의 onChange가 (selected: string | null, index: number) => void 로 변경되어 현재 호출부(단일 인자 핸들러)에 영향이 있습니다. tsconfig(strictFunctionTypes 등)에 따라 컴파일 오류가 발생할 수 있습니다.
- 수정 대상(직접 호출부):
- apps/client/src/shared/components/cardEditModal/CardEditModal.tsx (L111) — onChange={(value) => setSelected(value)}
- apps/extension/src/pages/MainPop.tsx (L281) — onChange={handleSelect} (handleSelect 시그니처 확인/수정 필요)
- packages/design-system/src/components/dropdown/Dropdown.stories.tsx (L71) — story 핸들러 업데이트 필요
- 조치: 호출부에서 두 번째 인자(index)를 받도록 핸들러 시그니처를 변경하거나, API 롤백 후 버전 범프(major)로 관리.
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/extension/src/apis/axiosInstance.ts (1)
1-160: 하드코딩된 JWT/테스트 이메일 제거 및 토큰 처리 보안 수정 필요리포에 하드코딩된 JWT와 테스트 이메일이 남아 있습니다. 즉시 삭제/대체하고 토큰 저장·재발급 로직을 안전하게 수정하세요.
- apps/extension/src/apis/axiosInstance.ts (lines 9–12): 주석 처리된 하드코딩 JWT 및 테스트 이메일 삭제(커밋 히스토리 검토).
- apps/extension/src/apis/axiosInstance.ts (line 84): fetchToken('test@gmail.com') 하드코딩 이메일 제거 — 이메일은 chrome.storage에서 안전하게 읽거나 호출자에게 전달하도록 변경.
- apps/extension/src/pages/MainPop.tsx (line 159): 주석된 테스트 이메일 삭제.
- apps/extension/src/apis/axiosInstance.ts (lines 36,49): chrome.storage.local.set/get 사용 검토 — 토큰 키명(token vs accessToken) 일관화, 'undefined' 문자열 처리 제거, 민감값 로컬 저장 지양(환경변수/시크릿 매니저 사용 권장).
- 전역 스캔으로 추가 시크릿/테스트 이메일(패턴 eyJ..., test@gmail*) 확인·제거하고, 노출된 토큰은 교체(rotate)하세요.
♻️ Duplicate comments (2)
apps/extension/src/apis/axiosInstance.ts (2)
83-86: 401/403 재시도 시 하드코딩된 이메일 사용 — 실제 사용자 이메일로 교체해야 합니다현재 'test@gmail.com'으로 토큰을 재발급해 모든 사용자가 오동작합니다.
적용 diff:
- originalRequest._retry = true; - const newToken = await fetchToken('test@gmail.com'); - originalRequest.headers.Authorization = `Bearer ${newToken}`; + originalRequest._retry = true; + const email = await new Promise<string | undefined>((resolve) => { + chrome.storage.local.get('email', (result) => resolve(result.email)); + }); + const newToken = await fetchToken(email); + originalRequest.headers.Authorization = `Bearer ${newToken}`;
9-17: 주석에 하드코딩된 JWT가 그대로 노출됨 — 즉시 삭제하고 서버 토큰도 회수(rotate)하세요주석이어도 레포에 남아 있으면 악용/경고 유발됩니다(현재 gitleaks가 탐지). 기록 정리(filter-repo/BFG)도 권장합니다.
적용 diff:
-// localStorage.setItem("token", 'eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJwaW5iYWNrIiwiaWQiOiI4NjA1NTBiMS1kZDBhLTQyMjMtYjM4OS0wNTEwYWU3MmNkMzUiLCJzdWIiOiJBY2Nlc3NUb2tlbiIsImV4cCI6MTc1NzYyOTAyMn0.qm-zqkuG2rpLlbUKJd9lUdh-4SStittgzXiwBeUMzA6NuKh_aEJmgoVInhUU-VSFtTlXP8eO9Ivao5K29LCRJA'); -// chrome.storage.local.set( -// { token: 'eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJwaW5iYWNrIiwiaWQiOiI4NjA1NTBiMS1kZDBhLTQyMjMtYjM4OS0wNTEwYWU3MmNkMzUiLCJzdWIiOiJBY2Nlc3NUb2tlbiIsImV4cCI6MTc1NzYyOTAyMn0.qm-zqkuG2rpLlbUKJd9lUdh-4SStittgzXiwBeUMzA6NuKh_aEJmgoVInhUU-VSFtTlXP8eO9Ivao5K29LCRJA', -// email:'test@gmail2.com' -// }, -// () => { -// console.log("토큰 저장 완료 ✅"); -// } -// );
🧹 Nitpick comments (7)
apps/extension/src/apis/axiosInstance.ts (7)
18-27: 레거시 주석 블록 정리주석으로 남은 과거 인터셉터 코드는 혼란만 가중합니다. 전부 제거해 주세요.
적용 diff:
-// apiRequest.interceptors.request.use((config) => { -// // signup은 토큰 필요 없음 -// if (config.url !== "/auth/signup") { -// const token = localStorage.getItem("accessToken"); -// if (token) { -// config.headers.Authorization = `Bearer ${token}`; -// } -// } -// return config; -// });
28-40: fetchToken: 쿼리 파라미터/로그/타임아웃 보완
- email이 undefined인 경우 쿼리에 'email=undefined'가 실리지 않도록 처리
- 불필요한 console.log는 dev에서만 출력
- 네트워크 타임아웃 추가
적용 diff:
const response = await axios.get( `${import.meta.env.VITE_BASE_URL}/api/v1/auth/token`, { - params: { email }, + // undefined가 쿼리로 전달되지 않도록 방지 + params: email ? { email } : {}, + timeout: 10000, } ); const newToken = response.data.data.token; chrome.storage.local.set({ token: newToken }, () => { - console.log('Token re-saved to chrome storage'); + if (import.meta.env.DEV) console.debug('[auth] token saved to chrome.storage'); });
3-8: axios 인스턴스에 기본 타임아웃을 설정하세요무한 대기 방지. 10초 권장.
적용 diff:
const apiRequest = axios.create({ baseURL: import.meta.env.VITE_BASE_URL, headers: { 'Content-Type': 'application/json', }, + timeout: 10000, });
42-67: 동시 요청 시 토큰 중복 발급 방지(인플라이트 합치기)토큰이 비어있는 초기 상태에서 여러 요청이 동시에 들어오면 fetchToken가 중복 호출됩니다. 단일 in-flight promise로 합치세요.
예시(파일 상단 모듈 스코프에 추가):
let tokenPromise: Promise<string> | null = null;인터셉터 내부 교체:
if (!token || token === 'undefined') { tokenPromise ||= (async () => { const t = await fetchToken(email); tokenPromise = null; return t!; })(); token = await tokenPromise; }
94-116: 타입 네이밍 PascalCase로 정렬(PostSignupRequest, PostCategoriesRequest)현재 일부 인터페이스가 소문자로 시작합니다. 컨벤션 일관화 제안.
적용 diff(후속 파일들 함께 반영 필요):
-export interface postSignupRequest { +export interface PostSignupRequest { email: string; remindDefault: string fcmToken: string; } -export const postSignup = async (data: postSignupRequest) => { +export const postSignup = async (data: PostSignupRequest) => { const response = await apiRequest.post("/api/v1/auth/signup", data); return response.data; }; -export interface postCategoriesRequest { +export interface PostCategoriesRequest { categoryName: string; } -export const postCategories = async (data: postCategoriesRequest) => { +export const postCategories = async (data: PostCategoriesRequest) => { const response = await apiRequest.post("/api/v1/categories", data); return response.data; }Also applies to: 123-131, 150-156
143-149: 코드 스타일: 공백/타입 표기 정리가독성을 위해 공백을 맞춰 주세요.
적용 diff:
-export const getArticleSaved=async (url:string) => { +export const getArticleSaved = async (url: string) => {
1-8: 환경 변수 누락 시 실패 방식 점검VITE_BASE_URL 미설정 시 조기 경고가 유용합니다.
예시:
if (!import.meta.env.VITE_BASE_URL && import.meta.env.DEV) { // eslint-disable-next-line no-console console.warn('[api] VITE_BASE_URL is not defined'); }
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/extension/src/apis/axiosInstance.ts(2 hunks)apps/extension/src/pages/MainPop.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/extension/src/pages/MainPop.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-11T11:48:10.615Z
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#75
File: apps/extension/src/apis/axiosInstance.ts:30-34
Timestamp: 2025-09-11T11:48:10.615Z
Learning: Pinback 프로젝트에서는 사용자 이메일 저장 시 'email' 키를 사용하도록 통일했습니다 (localStorage 및 chrome.storage.local 모두).
Applied to files:
apps/extension/src/apis/axiosInstance.ts
🪛 Gitleaks (8.27.2)
apps/extension/src/apis/axiosInstance.ts
[high] 9-9: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.
(jwt)
[high] 11-11: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.
(jwt)
🔇 Additional comments (4)
apps/extension/src/apis/axiosInstance.ts (4)
133-137: remind-time now 포맷: 타임존 요구사항 확인 필요현재 소수점 이하와 'Z'를 제거합니다. 서버가 UTC(Z) 기준을 기대한다면 오프셋 오류가 납니다. 스펙 확인 후 아래 중 하나로 통일하세요.
옵션 A(UTC 유지, ms 제거):
- const now = new Date().toISOString().split(".")[0]; + const now = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');옵션 B(그대로 ISO8601 전체 전달):
- const now = new Date().toISOString().split(".")[0]; + const now = new Date().toISOString();
49-51: 스토리지 키 'email' 사용 👍팀 컨벤션('email' 키) 준수 확인했습니다.
101-160: API 래퍼 전반: 엔드포인트/반환값 일관성 좋아요response.data만 노출하는 얇은 래퍼로 깔끔합니다.
69-90: AxiosHeaders 타입 호환성 확인 — headers 할당을 안전하게 변경
config.headers가 AxiosHeaders일 수 있으므로 config.headers.Authorization / originalRequest.headers.Authorization 직접 할당은 타입/런타임 문제를 일으킬 수 있습니다. apps/extension/src/apis/axiosInstance.ts의 request/response 인터셉터에서 해당 할당을 발견했습니다. 해결: AxiosHeaders.set('Authorization',Bearer ${token})를 사용하거나 config.headers = { ...(config.headers as Record<string,string>), Authorization:Bearer ${token}}처럼 평문 객체로 병합하여 설정하세요.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
apps/extension/src/pages/MainPop.tsx (2)
169-191: 동시 실행(save + post) 및 에러 제어 미흡 — mutateAsync 전환 권장로컬 save와 서버 post를 동시에 발사하고 예외 제어가 어려워집니다. mutateAsync로 전환하고 try/catch로 UX를 제어하세요. 상세 제안은 이전 코멘트와 동일합니다.
148-167: 리마인드 검증 가드 추가isRemindOn인 경우 date/time 에러가 존재하면 저장을 막아주세요. 현재는 그대로 진행되어 API 에러/데이터 불일치가 발생할 수 있습니다.
const handleSave = async () => { const currentDate = date; const currentTime = time; if (!selected || parseInt(selected) === 0) { alert("카테고리를 선택해주세요!"); return; } + if (isRemindOn) { + const de = validateDate(currentDate); + const te = validateTime(currentTime); + setDateError(de); + setTimeError(te); + if (de || te) { + alert("리마인드 날짜/시간을 올바르게 입력해주세요."); + return; + } + }
🧹 Nitpick comments (11)
apps/extension/src/types/types.ts (2)
1-5: Category/CategoryResponse 중복 정의 제거 제안동일 필드가 두 군데에 중복 정의되어 드리프트 위험이 있습니다. 공통 Base 타입을 두고 별칭으로 사용하는 편이 안전합니다.
-export interface CategoryResponse { - categoryId: number; - categoryName: string; - categoryColor: string; -} +export interface BaseCategory { + categoryId: number; + categoryName: string; + categoryColor: string; +} +export type CategoryResponse = BaseCategory; ... -export interface Category { - categoryId: number; - categoryName: string; - categoryColor:string; - } +export type Category = BaseCategory;Also applies to: 16-20
7-14: 날짜/시간 문자열 포맷 계약 명시 필요createdAt, remindAt가 어떤 포맷(예: ISO 8601, 서버 로케일 시간 등)인지 주석/브랜디드 타입으로 명시해 주세요. 런타임 파싱 오류를 줄일 수 있습니다.
packages/design-system/src/components/dateTime/DateTime.tsx (1)
170-178: placeholder/패턴 정합성 및 정규식 앵커 보강
- placeholder가 ‘YY.MM.DD’로 되어 있으나 실제 포맷은 ‘YYYY.MM.DD’입니다.
- pattern은 명시적 앵커(^, $)를 두는 편이 오해를 줄입니다.
- time placeholder도 실제 표시 포맷(“오전 09:30”)과 맞추는 것을 권장합니다.
- placeholder={type === 'date' ? 'YY.MM.DD' : 'HH:MM'} + placeholder={type === 'date' ? 'YYYY.MM.DD' : '오전 09:30'} ... - ? '\\d{4}\\.\\d{2}\\.\\d{2}' - : '(오전|오후)\\s\\d{2}:\\d{2}' + ? '^\\d{4}\\.\\d{2}\\.\\d{2}$' + : '^(오전|오후)\\s\\d{2}:\\d{2}$'apps/extension/src/utils/remindTimeFormat.ts (2)
13-28: to24Hour: ‘HHmm’/‘HH:mm’ 순수 24시 입력 보강UI 외부에서 진입하는 값 대비 안전망을 두면 좋습니다. ‘1345’ → ‘13:45’, ‘13:45’ → 그대로 등 케이스를 흡수하세요.
export const to24Hour = (time: string) => { - // "오전 11:23" | "오후 11:23" 같은 문자열 들어온다고 가정 + // 우선 24시간 표기 처리 + if (/^\d{2}:\d{2}$/.test(time)) { + return time; // 이미 HH:mm + } + if (/^\d{4}$/.test(time)) { + return `${time.slice(0,2)}:${time.slice(2,4)}`; // HHmm -> HH:mm + } + // "오전 11:23" | "오후 11:23" 같은 문자열 들어온다고 가정 const match = time.match(/(오전|오후)\s(\d{1,2}):(\d{2})/); if (!match) return time; // 포맷 안 맞으면 그대로 반환
30-38: combineDateTime: 일관된 ISO 포맷 보장현재 ‘HHmm’ 같은 비정상 입력이 들어오면 ‘...T1345’가 될 수 있습니다. 위 to24Hour 보강과 함께, 최종적으로 항상 ‘YYYY-MM-DDTHH:mm:ss’ 형태가 되도록 방어 로직을 추가해 주세요.
- const normalizedTime = to24Hour(time); // ✅ AM/PM 처리 - const formattedTime = normalizedTime.length === 5 ? `${normalizedTime}:00` : normalizedTime; + const normalizedTime = to24Hour(time); + // HH:mm:ss 보장 + const hhmm = /^\d{2}:\d{2}$/.test(normalizedTime) + ? `${normalizedTime}:00` + : normalizedTime; + const formattedTime = /^\d{2}:\d{2}:\d{2}$/.test(hhmm) ? hhmm : `${hhmm}:00`;apps/extension/src/hooks/useCategoryManager.ts (4)
14-16: 옵션 목록 동기화 전략 확인options는 서버 fetch 결과만을 사용합니다. 새 카테고리 생성 후 리스트가 즉시 반영되지 않으므로 invalidate를 통해 재조회하는 편이 UX가 좋습니다.
17-22: 입력 검증 보강(트림/빈값/중복/예약어 차단)
- 공백만 입력, 중복 이름, 팀 규칙상 예약어(“안 읽은 정보”)를 차단해 주세요. 학습 메모에 따른 일관성 유지 필요.
- const saveCategory = (onSuccess?: (category: Category) => void) => { - if (categoryTitle.length > 20) { + const saveCategory = (onSuccess?: (category: Category) => void) => { + const title = categoryTitle.trim(); + if (title.length === 0) { + setIsPopError(true); + setErrorTxt("한 글자 이상 입력해주세요"); + return; + } + if (title.length > 20) { setIsPopError(true); setErrorTxt("20자 이내로 작성해주세요"); return; } + const names = categoryData?.data?.categories?.map((c: Category) => c.categoryName) ?? []; + if (names.includes(title)) { + setIsPopError(true); + setErrorTxt("이미 존재하는 카테고리입니다"); + return; + } + if (title === "안 읽은 정보") { + setIsPopError(true); + setErrorTxt("기본 카테고리 이름은 사용할 수 없어요"); + return; + }
24-41: mutate → mutateAsync 전환 및 목록 무효화성공 후 목록 재조회와 에러 흐름 제어를 위해 mutateAsync + invalidate 권장. 또한 서버 정규화 결과를 그대로 사용하세요.
-import { usePostCategories, useGetCategoriesExtension } from "@apis/query/queries"; +import { usePostCategories, useGetCategoriesExtension } from "@apis/query/queries"; +import { useQueryClient } from "@tanstack/react-query"; ... - const { mutate: postCategories } = usePostCategories(); + const { mutateAsync: postCategories } = usePostCategories(); + const queryClient = useQueryClient(); ... - postCategories( - { categoryName: categoryTitle }, - { - onSuccess: (res) => { - const newCategory: Category = { - categoryId: res.data.categoryId, - categoryName: categoryTitle, - categoryColor: res.data.categoryColor ?? "#000000", - }; - onSuccess?.(newCategory); - resetPopup(); - }, - onError: (err: AxiosError<{ code: string; message: string }>) => { - alert(err.response?.data?.message ?? "카테고리 추가 중 오류가 발생했어요 😢"); - }, - } - ); + postCategories({ categoryName: title }) + .then(async (res) => { + const newCategory: Category = { + categoryId: res.data.categoryId, + categoryName: res.data.categoryName ?? title, + categoryColor: res.data.categoryColor ?? "#000000", + }; + onSuccess?.(newCategory); + await queryClient.invalidateQueries({ queryKey: ["categoriesExtension"] }); + resetPopup(); + }) + .catch((err: AxiosError<{ code: string; message: string }>) => { + alert(err.response?.data?.message ?? "카테고리 추가 중 오류가 발생했어요 😢"); + });
37-38: alert 기반 에러 처리 최소화Hook 레이어에서는 alert보다는 에러 상태를 set하여 호출자가 UI로 표시하도록 위임하는 편이 재사용성이 좋습니다.
apps/extension/src/pages/MainPop.tsx (2)
112-116: Dropdown 인덱스-의존 매핑은 중복명에 취약동일한 이름의 카테고리가 있을 경우 잘못된 id가 매핑될 수 있습니다. options에 id를 실어 value 기반으로 선택/변환하는 구조를 권장합니다.
- const handleSelect = (value: string | null, idx: number) => { - const categoryId = categoryData?.data?.categories[idx]?.categoryId.toString() ?? null; - setSelected(categoryId); - setSelectedCategoryName(value); - }; + // Dropdown 컴포넌트가 value(id) 전달을 지원한다면: + const handleSelect = (label: string | null, _idx: number, value?: string | null) => { + setSelected(value ?? null); + setSelectedCategoryName(label); + };
301-303: 저장 버튼 비활성화 조건 추가로딩/검증 에러/저장 중에는 비활성화가 필요합니다. saving 상태를 두고 UX를 안정화하세요.
- <Button size="medium" onClick={handleSave}> + <Button + size="medium" + onClick={handleSave} + disabled={ + loading || + (isRemindOn && (!!dateError || !!timeError)) + } + > 저장 </Button>
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
apps/extension/src/hooks/useCategoryManager.ts(1 hunks)apps/extension/src/pages/MainPop.tsx(1 hunks)apps/extension/src/types/types.ts(1 hunks)apps/extension/src/utils/remindTimeFormat.ts(1 hunks)apps/extension/tsconfig.json(1 hunks)packages/design-system/src/components/dateTime/DateTime.tsx(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/extension/tsconfig.json
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-17T09:18:13.818Z
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#102
File: apps/extension/src/components/modalPop/ModalPop.tsx:166-172
Timestamp: 2025-07-17T09:18:13.818Z
Learning: In apps/extension/src/components/modalPop/ModalPop.tsx, the categories array should include "안 읽은 정보" (Unread Information) as the first default category that cannot be deleted. This default category is used consistently across the client-side dashboard and should be protected from deletion in the extension as well.
Applied to files:
apps/extension/src/hooks/useCategoryManager.tsapps/extension/src/pages/MainPop.tsx
🧬 Code graph analysis (3)
packages/design-system/src/components/dateTime/DateTime.tsx (1)
packages/design-system/src/components/dateTime/utils/FormatData.ts (3)
formatDate(7-16)digitsOnly(2-4)formatTime12(19-45)
apps/extension/src/hooks/useCategoryManager.ts (3)
apps/extension/src/apis/query/queries.ts (2)
useGetCategoriesExtension(39-44)usePostCategories(28-38)apps/extension/src/apis/axiosInstance.ts (1)
postCategories(127-130)apps/extension/src/types/types.ts (1)
Category(16-20)
apps/extension/src/pages/MainPop.tsx (8)
apps/extension/src/types/types.ts (1)
ArticleResponse(7-14)apps/extension/src/apis/axiosInstance.ts (2)
postArticle(101-104)putArticle(157-160)apps/extension/src/apis/query/queries.ts (4)
usePostArticle(4-14)usePutArticle(61-72)useGetCategoriesExtension(39-44)useGetRemindTime(46-51)apps/extension/src/hooks/usePageMeta.ts (1)
usePageMeta(21-61)apps/extension/src/hooks/useSaveBookmarks.ts (1)
useSaveBookmark(13-50)apps/extension/src/utils/remindTimeFormat.ts (3)
updateDate(2-5)updateTime(8-11)combineDateTime(30-38)apps/extension/src/hooks/useCategoryManager.ts (1)
useCategoryManager(6-58)packages/design-system/src/components/dateTime/DateTime.tsx (1)
DateTime(55-184)
🔇 Additional comments (4)
packages/design-system/src/components/dateTime/DateTime.tsx (2)
160-169: time 경로 onBlur에서 onChange 호출 — 이전 이슈 해결 확인time 타입도 onBlur 시 부모 onChange가 호출되어 상위 상태가 갱신됩니다. 이전 “onChange 미호출” 문제는 해결된 것으로 보입니다. 👍
68-70: onBlur 기반 동기화 UX 확인 요청입력 중 즉시 검증/동기화가 아닌 blur 시점에만 상위로 반영됩니다. MainPop의 즉시 에러 표시에 의존한다면 의도한 UX인지 확인이 필요합니다.
Also applies to: 155-169
apps/extension/src/pages/MainPop.tsx (2)
244-253: 기본 카테고리 “안 읽은 정보” 존재 보장 확인팀 룰에 따라 첫 번째 기본 카테고리가 항상 존재/삭제 불가해야 합니다. 서버/클라 어느 쪽에서 보장하는지 확인해 주세요.
275-294: DateTime(onBlur 동기화)와 실시간 에러 메시지 UX 확인현재 에러 메시지는 blur 이후에만 갱신됩니다. 즉시 검증이 필요하면 design-system 컴포넌트 onChange에서도 포맷팅/전달을 고려하세요.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (4)
packages/design-system/src/components/dateTime/DateTime.tsx (4)
59-60: onChange를 blur 시점으로만 발화 → 기존 소비처 브레이킹 가능성 확인 필요입력 중에는 부모 onChange가 호출되지 않습니다. 이 변경이 모든 사용처(특히 실시간 유효성 표시·버튼 활성화 등)에 영향 없는지 확인해 주세요. 필요하면 즉시 발화/blur 확정을 선택할 수 있는 옵션(예: commit="immediate" | "blur", 기본 "immediate") 도입을 고려해 보세요.
67-71: 12시간 표기(오전/오후) vs 24시간 입력 UX 혼선표시는 12시간제인데 placeholder는 “HH:MM”(24시간처럼 보임)이라 혼동 우려가 있습니다. 예: placeholder를 '오전 HH:MM' 또는 'HH:MM (24h)'로 조정하거나 AM/PM 토글 제공을 검토해 주세요.
73-76: 타이핑 중 부모 onChange 미발화 영향 점검현재는 내부 표시만 갱신합니다. 실시간 검증/의존 로직이 있는 화면에서 회귀가 없는지 확인해 주세요. 필요 시 선택적 즉시 발화 옵션을 도입하세요(예: commit="immediate"일 때 handleChange에서 onChange 호출).
95-99: 접근성 보강: aria 속성 추가 권장시각 레이블(span)과 입력의 연결이 없어 스크린 리더 접근성이 떨어집니다. 간단히 aria-label/aria-invalid만 추가해도 개선됩니다.
<input ref={inputRef} type="text" className={dateTimeTxtStyles({ state })} value={formatted} onChange={handleChange} onBlur={handleBlur} - placeholder={type === 'date' ? 'YYYY.MM.DD' : 'HH:MM'} + placeholder={type === 'date' ? 'YYYY.MM.DD' : 'HH:MM'} + aria-label={type === 'date' ? '날짜' : '시간'} + aria-invalid={state === 'error'}추가로 여유가 있으면 useId로 span에 id를 부여하고 input에 aria-labelledby를 연결하는 방식이 더 바람직합니다.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
apps/extension/src/pages/MainPop.tsx(1 hunks)apps/extension/src/utils/remindTimeFormat.ts(1 hunks)packages/design-system/src/components/dateTime/DateTime.tsx(2 hunks)packages/design-system/src/components/dateTime/utils/ValidateData.ts(2 hunks)
✅ Files skipped from review due to trivial changes (1)
- packages/design-system/src/components/dateTime/utils/ValidateData.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/extension/src/pages/MainPop.tsx
- apps/extension/src/utils/remindTimeFormat.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/design-system/src/components/dateTime/DateTime.tsx (1)
packages/design-system/src/components/dateTime/utils/FormatData.ts (3)
digitsOnly(2-4)formatDate(7-16)formatTime12(19-45)
🔇 Additional comments (1)
packages/design-system/src/components/dateTime/DateTime.tsx (1)
105-105: 마무리반환 JSX 구조는 문제 없어 보입니다.
| // rawDigits → 숫자만 관리 | ||
| const [rawDigits, setRawDigits] = useState(() => digitsOnly(value)); | ||
|
|
There was a problem hiding this comment.
외부 value 변경 시 내부 상태(rawDigits) 미동기화 버그
초기 마운트 이후 prop value가 바뀌어도 rawDigits가 갱신되지 않아 표시 값이 오래될 수 있습니다. 아래 효과로 동기화해 주세요.
const [rawDigits, setRawDigits] = useState(() => digitsOnly(value));
+ // 외부 value 변화에 동기화
+ useEffect(() => {
+ setRawDigits(digitsOnly(value ?? ''));
+ }, [value]);추가로 import에 useEffect를 포함해야 합니다:
import { useRef, useState, useEffect, type ChangeEvent } from 'react';🤖 Prompt for AI Agents
In packages/design-system/src/components/dateTime/DateTime.tsx around lines
64-66: the component initializes rawDigits from props with useState but never
updates it when the incoming prop value changes, causing the displayed value to
become stale; add a useEffect that watches the value prop and calls
setRawDigits(digitsOnly(value)) to keep internal state in sync, and update the
import line to include useEffect (import { useRef, useState, useEffect, type
ChangeEvent } from 'react';).
| const handleBlur = () => { | ||
| if (type === 'date') { | ||
| onChange?.(formatDate(rawDigits.slice(0, 8))); | ||
| } else { | ||
| onChange?.(formatTime12(rawDigits.slice(0, 4))); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
유효성 검증 없이 blur 시 값 확정 → 잘못된 날짜/시간 확정 위험
월(1–12), 일(1–31), 시(0–23), 분(0–59) 범위 검증 없이 포매팅만 해서 부모로 전달됩니다. 최소 범위 체크 후에만 onChange를 호출하거나, 잘못된 경우 빈 문자열/에러 상태로 처리해 주세요.
const handleBlur = () => {
- if (type === 'date') {
- onChange?.(formatDate(rawDigits.slice(0, 8)));
- } else {
- onChange?.(formatTime12(rawDigits.slice(0, 4)));
- }
+ if (type === 'date') {
+ const d = rawDigits.slice(0, 8);
+ const y = d.slice(0, 4), m = Number(d.slice(4, 6) || 0), dd = Number(d.slice(6, 8) || 0);
+ const valid = d.length === 8 && m >= 1 && m <= 12 && dd >= 1 && dd <= 31;
+ onChange?.(valid ? formatDate(d) : '');
+ } else {
+ const t = rawDigits.slice(0, 4);
+ const hh = Number(t.slice(0, 2) || 0), mm = Number(t.slice(2, 4) || 0);
+ const valid = t.length === 4 && hh >= 0 && hh <= 23 && mm >= 0 && mm <= 59;
+ onChange?.(valid ? formatTime12(t) : '');
+ }
};가능하다면 ValidateData 유틸의 검증 루틴을 재사용하는 편이 더 일관됩니다.
📝 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 handleBlur = () => { | |
| if (type === 'date') { | |
| onChange?.(formatDate(rawDigits.slice(0, 8))); | |
| } else { | |
| onChange?.(formatTime12(rawDigits.slice(0, 4))); | |
| } | |
| const handleBlur = () => { | |
| if (type === 'date') { | |
| const d = rawDigits.slice(0, 8); | |
| const y = d.slice(0, 4), m = Number(d.slice(4, 6) || 0), dd = Number(d.slice(6, 8) || 0); | |
| const valid = d.length === 8 && m >= 1 && m <= 12 && dd >= 1 && dd <= 31; | |
| onChange?.(valid ? formatDate(d) : ''); | |
| } else { | |
| const t = rawDigits.slice(0, 4); | |
| const hh = Number(t.slice(0, 2) || 0), mm = Number(t.slice(2, 4) || 0); | |
| const valid = t.length === 4 && hh >= 0 && hh <= 23 && mm >= 0 && mm <= 59; | |
| onChange?.(valid ? formatTime12(t) : ''); | |
| } | |
| }; |
🤖 Prompt for AI Agents
In packages/design-system/src/components/dateTime/DateTime.tsx around lines 78
to 83, the blur handler currently always formats and emits values without
validating ranges; update handleBlur to validate the parsed date/time components
(month 1–12, day 1–31 with month/year-aware checks if possible, hour 0–23,
minute 0–59) before calling onChange: reuse the existing ValidateData utility
validation routine if available (call it with the assembled value or parsed
parts) and only call onChange with formatDate(format) or formatTime12(format)
when validation passes; if validation fails, call onChange with an empty string
(or trigger the component error state) so invalid values are not confirmed to
the parent.
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/design-system/src/components/dateTime/DateTime.stories.tsx (2)
89-95: 동일 이슈: Time_Backspace도 라벨 쿼리 실패 가능위와 같은 이유로 이 play도 실패합니다. 동일하게 placeholder 기반으로 교체해주세요.
- const input = await canvas.findByLabelText('시간 입력'); + const input = await canvas.findByPlaceholderText('HH:MM');
74-79: play 단계 쿼리 불일치: findByLabelText('시간 입력')는 실패합니다컴포넌트에 해당 aria-label/label 연계가 없어 현재 쿼리는 요소를 찾지 못합니다. placeholder 기반 쿼리로 교체하거나(아래 diff), 컴포넌트에 접근성 라벨을 추가하는 방식 둘 중 하나가 필요합니다.
- const input = await canvas.findByLabelText('시간 입력'); + const input = await canvas.findByPlaceholderText('HH:MM');검증 스크립트(컴포넌트에 aria-label이 없는지 확인):
#!/bin/bash # DateTime에 aria-label/label 연계가 없는지, 스토리에서 '시간 입력'을 찾고 있는지 확인 rg -nP -C2 --type=ts 'aria-label|<label|htmlFor|aria-labelledby' packages/design-system/src/components/dateTime/DateTime.tsx rg -nP -n --type=ts "findByLabelText\\('시간 입력'\\)" packages/design-system/src/components/dateTime/DateTime.stories.tsx선택적으로 컴포넌트 자체 개선(권장, 별도 파일 변경):
// packages/design-system/src/components/dateTime/DateTime.tsx // input props에 추가 aria-label={type === 'date' ? '날짜 입력' : '시간 입력'}
🧹 Nitpick comments (1)
packages/design-system/src/components/dateTime/DateTime.stories.tsx (1)
43-47: Controlled의 any 타입 제거 + 값 동기화 누락 보완
- lint 경고(Unexpected any)를 없애고, Storybook Controls에서 value를 바꿀 때 내부 상태가 동기화되도록 해주세요.
-import { useState } from 'react'; +import { useEffect, useState, type ComponentProps } from 'react'; /** 📌 컨트롤 가능한 Wrapper */ -const Controlled = (args: any) => { - const [val, setVal] = useState(args.value); - return <DateTime {...args} value={val} onChange={setVal} />; -}; +type ControlledProps = ComponentProps<typeof DateTime>; +const Controlled = (args: ControlledProps) => { + const [val, setVal] = useState(args.value); + useEffect(() => setVal(args.value), [args.value]); + return <DateTime {...args} value={val} onChange={setVal} />; +};Also applies to: 3-3
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
packages/design-system/src/components/dateTime/DateTime.stories.tsx(5 hunks)packages/design-system/src/icons/iconNames.ts(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/design-system/src/icons/iconNames.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/design-system/src/components/dateTime/DateTime.stories.tsx (1)
packages/design-system/src/components/dateTime/DateTime.tsx (1)
DateTime(55-105)
🪛 GitHub Check: lint
packages/design-system/src/components/dateTime/DateTime.stories.tsx
[warning] 44-44:
Unexpected any. Specify a different type
🔇 Additional comments (4)
packages/design-system/src/components/dateTime/DateTime.stories.tsx (4)
26-27: 문서 설명 업데이트 적절함
- value는 "숫자만"을 초기값으로 받는다는 점 명확.
- 입력 중 내부 포맷팅, blur 시 최종 전달이라는 핵심 동작을 정확히 기술.
Also applies to: 34-35
49-56: Date_Default: Controlled 래핑 전환 LGTM스토리 목적(내부 포맷팅 + blur 전달)과 일치합니다.
58-66: Time_Empty: 초기 시간 입력 시나리오 정의 적절컨트롤러 패턴과 초기값 설정이 명확합니다.
98-105: States_Showcase: 고정 그리드/컨트롤 제외 전환 좋습니다상태 프리뷰 용도에 맞게 args 의존성을 제거한 판단이 합리적입니다.
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/design-system/src/components/dateTime/DateTime.tsx (1)
90-101: 접근성: 레이블 미연결로 스토리 상호작용 실패 가능컴포넌트에서 aria-label/label 연결이 없어 Story의 findByLabelText('시간 입력')가 실패합니다. 입력에 접근 가능한 이름을 부여해 주세요.
<input ref={inputRef} type="text" className={dateTimeTxtStyles({ state })} value={formatted} onChange={handleChange} onBlur={handleBlur} + aria-label={type === 'date' ? '날짜 입력' : '시간 입력'} placeholder={type === 'date' ? 'YYYY.MM.DD' : 'HH:MM'} inputMode="numeric" disabled={isDisabled} maxLength={type === 'date' ? 10 : 8} />대안: span 대신 label + htmlFor로 명시적 연결을 하는 방법도 있습니다(선호).
♻️ Duplicate comments (2)
packages/design-system/src/components/dateTime/DateTime.tsx (2)
64-66: 외부 value 변경 시 내부 상태(rawDigits) 동기화 누락초기 마운트 이후 prop value가 변해도 rawDigits가 갱신되지 않아 표시값이 오래됩니다. 아래 효과로 동기화해 주세요. (기존 코멘트와 동일 이슈)
// rawDigits → 숫자만 관리 const [rawDigits, setRawDigits] = useState(() => digitsOnly(value)); + // 외부 value 동기화 + useEffect(() => { + setRawDigits(digitsOnly(value ?? '')); + }, [value]);아울러 import에 useEffect 추가가 필요합니다(파일 상단):
import { useRef, useState, useEffect, type ChangeEvent } from 'react';
77-82: 유효성 검증 없이 blur 시 값 확정됨월/일/시/분 범위 검증이 없어 잘못된 값이 부모로 확정 전달될 수 있습니다. blur 시 간단한 범위 및 날짜 유효성 체크 후에만 값을 emit해 주세요. (기존 코멘트와 동일 이슈)
- const handleBlur = () => { - if (type === 'date') { - onChange?.(formatDate(rawDigits.slice(0, 8))); - } else { - onChange?.(formatTime12(rawDigits.slice(0, 4))); - } - }; + const handleBlur = () => { + if (type === 'date') { + const d = rawDigits.slice(0, 8); + const y = d.slice(0, 4); + const m = Number(d.slice(4, 6)); + const dd = Number(d.slice(6, 8)); + const lenOk = d.length === 8; + const dateOk = + lenOk && + m >= 1 && m <= 12 && + dd >= 1 && dd <= 31 && + (() => { + const dt = new Date(Number(y), m - 1, dd); + return ( + dt.getFullYear() === Number(y) && + dt.getMonth() === m - 1 && + dt.getDate() === dd + ); + })(); + onChange?.(dateOk ? formatDate(d) : ''); + } else { + const t = rawDigits.slice(0, 4); + const hh = Number(t.slice(0, 2)); + const mm = Number(t.slice(2, 4)); + const timeOk = t.length === 4 && hh >= 0 && hh <= 23 && mm >= 0 && mm <= 59; + onChange?.(timeOk ? formatTime12(t) : ''); + } + };
🧹 Nitpick comments (2)
packages/design-system/src/components/dateTime/DateTime.tsx (1)
55-61: onChange 네이밍이 실제 동작(onBlur 커밋)과 불일치콜백이 입력 변경 시점이 아니라 blur 시점에만 호출됩니다. API 혼동 방지를 위해 onCommit/onValueCommit 등 명확한 이름 도입 또는 JSDoc에 “blur 시 확정 전달”을 명시하는 것을 권장합니다.
packages/design-system/src/components/dateTime/DateTime.stories.tsx (1)
3-3: Story Controlled 래퍼가 args.value 변경을 반영하지 않음Controls에서 value를 바꿔도 내부 상태가 갱신되지 않습니다. 동기화 효과를 추가해 주세요.
-import { useState } from 'react'; +import { useEffect, useState } from 'react'; @@ const Controlled = (args: React.ComponentProps<typeof DateTime>) => { const [val, setVal] = useState(args.value); + useEffect(() => setVal(args.value), [args.value]); return <DateTime {...args} value={val} onChange={setVal} />; };Also applies to: 43-47
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
packages/design-system/src/components/dateTime/DateTime.stories.tsx(5 hunks)packages/design-system/src/components/dateTime/DateTime.tsx(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
packages/design-system/src/components/dateTime/DateTime.tsx (1)
packages/design-system/src/components/dateTime/utils/FormatData.ts (3)
digitsOnly(2-4)formatDate(7-16)formatTime12(19-45)
packages/design-system/src/components/dateTime/DateTime.stories.tsx (1)
packages/design-system/src/components/dateTime/DateTime.tsx (1)
DateTime(55-104)
🔇 Additional comments (1)
packages/design-system/src/components/dateTime/DateTime.stories.tsx (1)
75-79: findByLabelText 쿼리 신뢰성 확인 필요컴포넌트가 aria-label을 제공하지 않으면 이 쿼리는 실패합니다. 컴포넌트에 aria-label을 추가(권장, 제안 반영 시 그대로 통과)하거나, 스토리를 placeholder 기반으로 수정해 주세요.
대안 패치(컴포넌트 수정이 어렵다면):
- const input = await canvas.findByLabelText('시간 입력'); + const input = await canvas.findByPlaceholderText('HH:MM');
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (4)
apps/extension/src/apis/query/queries.ts (4)
1-2: useQueryClient 임포트 누락 — 캐시 무효화 준비 필요invalidateQueries 적용을 위해 useQueryClient를 함께 임포트하세요.
-import { useMutation,useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
4-14: postArticle 성공 시 관련 캐시 무효화 + 콘솔 Lint 경고 제거저장 후 ["articleSaved", data.url] 무효화가 없어서 UI가 갱신되지 않습니다. 콘솔은 NODE_ENV로 가드하세요.
export const usePostArticle = () => { - return useMutation({ + const queryClient = useQueryClient(); + return useMutation({ mutationFn: (data: PostArticleRequest) => postArticle(data), onSuccess: (data) => { - console.log("저장 성공:", data); + if (data?.url) { + queryClient.invalidateQueries({ queryKey: ["articleSaved", data.url] }); + } + if (process.env.NODE_ENV !== "production") { + // eslint-disable-next-line no-console + console.log("저장 성공:", data); + } }, onError: (error) => { - console.error("저장 실패:", error); + if (process.env.NODE_ENV !== "production") { + // eslint-disable-next-line no-console + console.error("저장 실패:", error); + } }, }); };
28-38: 카테고리 생성 후 목록 캐시 무효화 누락생성 성공 시 ["categoriesExtension"]을 invalidate하여 드롭다운이 즉시 반영되게 하세요. 콘솔도 가드합니다.
export const usePostCategories = () => { - return useMutation({ + const queryClient = useQueryClient(); + return useMutation({ mutationFn: (data: postCategoriesRequest) => postCategories(data), onSuccess: (data) => { - console.log("카테고리 저장", data); + queryClient.invalidateQueries({ queryKey: ["categoriesExtension"] }); + if (process.env.NODE_ENV !== "production") { + // eslint-disable-next-line no-console + console.log("카테고리 저장", data); + } }, onError: (error) => { - console.error("카테고리 저장 실패", error); + if (process.env.NODE_ENV !== "production") { + // eslint-disable-next-line no-console + console.error("카테고리 저장 실패", error); + } }, }); }
53-59: 내부/비HTTP URL 차단 + 깜빡임 방지 옵션chrome://, edge://, file:, data: 등은 호출을 막고, 포커스 리패치/짧은 staleTime으로 중복 팝업 깜빡임을 줄이세요.
export const useGetArticleSaved = (url:string) => { - return useQuery({ + const isHttpUrl = /^https?:\/\//i.test(url); + return useQuery({ queryKey: ["articleSaved", url], queryFn: () => getArticleSaved(url), - enabled: !!url, + enabled: !!url && isHttpUrl, + refetchOnWindowFocus: false, + staleTime: 30 * 1000, }); }
🧹 Nitpick comments (6)
apps/extension/src/apis/query/queries.ts (3)
16-26: 회원가입 훅: 콘솔 가드 및 후속 캐시 고려콘솔을 NODE_ENV로 가드하고, 필요 시 온보딩 관련 쿼리(예: ["remindTime"]) 무효화도 검토하세요.
onSuccess: (data) => { - console.log("회원가입 성공:", data); + if (process.env.NODE_ENV !== "production") { + // eslint-disable-next-line no-console + console.log("회원가입 성공:", data); + } }, onError: (error) => { - console.error("회원가입 실패:", error); + if (process.env.NODE_ENV !== "production") { + // eslint-disable-next-line no-console + console.error("회원가입 실패:", error); + } },
46-51: 리마인드타임 쿼리: 포커스 리패치 비활성화 권장확장 팝업 재포커스 시 깜빡임 방지를 위해 refetchOnWindowFocus와 staleTime을 설정하세요.
return useQuery({ queryKey: ["remindTime"], queryFn: getRemindTime, + refetchOnWindowFocus: false, + staleTime: 60 * 1000, });
4-72: React Query 제네릭/리턴타입 지정으로 any 축소axios 래퍼가 response.data(any) 반환이라 훅의 결과도 any로 흐릅니다. 서버 응답 타입 정의 후 useQuery/useMutation에 제네릭을 지정해 타입 안정성을 높이세요.
apps/extension/src/apis/axios.ts (3)
15-19: 인터페이스 네이밍 일관성 (대문자 PascalCase 권장)postSignupRequest → PostSignupRequest로 변경해 다른 타입들과 일관성을 맞추세요. 참조부도 함께 수정 필요.
-export interface postSignupRequest { +export interface PostSignupRequest { email: string; remindDefault: string fcmToken: string; }참조 업데이트 예: queries.ts의 import/useMutation 제네릭 등.
31-33: 동일 이슈: postCategoriesRequest → PostCategoriesRequest카멜케이스 불일치로 혼동 여지. 인터페이스/사용처 동시 변경 권장.
-export interface postCategoriesRequest { +export interface PostCategoriesRequest { categoryName: string; }
9-12: API 반환 타입 명시 권장각 함수의 반환 타입을 구체화해 상위 훅에서 타입 안전성을 확보하세요. 예: Promise.
Also applies to: 21-24, 35-38, 50-56, 65-68
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
apps/extension/src/apis/axios.ts(1 hunks)apps/extension/src/apis/query/queries.ts(1 hunks)packages/design-system/src/components/dateTime/DateTime.stories.tsx(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/design-system/src/components/dateTime/DateTime.stories.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-17T09:18:13.818Z
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#102
File: apps/extension/src/components/modalPop/ModalPop.tsx:166-172
Timestamp: 2025-07-17T09:18:13.818Z
Learning: In apps/extension/src/components/modalPop/ModalPop.tsx, the categories array should include "안 읽은 정보" (Unread Information) as the first default category that cannot be deleted. This default category is used consistently across the client-side dashboard and should be protected from deletion in the extension as well.
Applied to files:
apps/extension/src/apis/query/queries.ts
🧬 Code graph analysis (1)
apps/extension/src/apis/query/queries.ts (1)
apps/extension/src/apis/axios.ts (11)
PostArticleRequest(2-7)postArticle(9-12)postSignupRequest(15-19)postSignup(21-24)postCategoriesRequest(31-33)postCategories(35-38)getCategoriesExtension(26-29)getRemindTime(40-48)getArticleSaved(51-56)PutArticleRequest(58-63)putArticle(65-68)
🪛 GitHub Check: lint
apps/extension/src/apis/query/queries.ts
[warning] 69-69:
Unexpected console statement
[warning] 66-66:
Unexpected console statement
[warning] 35-35:
Unexpected console statement
[warning] 32-32:
Unexpected console statement
[warning] 23-23:
Unexpected console statement
[warning] 20-20:
Unexpected console statement
[warning] 11-11:
Unexpected console statement
[warning] 8-8:
Unexpected console statement
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (2)
apps/extension/src/apis/query/queries.ts (1)
39-44: 카테고리 기본값(“안 읽은 정보”) 보장 여부 확인대시보드와 일관성을 위해 첫 항목에 삭제 불가 기본 카테고리 “안 읽은 정보”가 포함되는지 확인/강제 정렬을 고려하세요. (과거 러닝 참고)
apps/extension/src/apis/axios.ts (1)
58-63: PutArticleRequest.memo null 허용 여부 확인POST는 memo?: string | null인데 PUT은 memo: string(필수)입니다. 빈 메모 편집 시 서버 검증 오류 가능. 서버 스펙에 맞춰 null 허용/선택값으로 정렬하세요.
| export const getRemindTime = async () => { | ||
| const now = new Date().toISOString().split(".")[0]; | ||
|
|
||
| const response = await apiRequest.get("/api/v1/users/remind-time", { | ||
| params: { now }, | ||
| }); | ||
|
|
||
| return response.data; | ||
| }; |
There was a problem hiding this comment.
now 포맷에서 타임존 소실 — 잘못된 시간 해석 위험
toISOString().split(".")[0]는 밀리초와 ‘Z’(UTC)를 제거해 서버가 로컬 시각으로 오해할 수 있습니다. 전체 ISO(또는 초 단위+Z 유지)로 전송하세요.
-export const getRemindTime = async () => {
- const now = new Date().toISOString().split(".")[0];
+export const getRemindTime = async () => {
+ const now = new Date().toISOString(); // 예: 2025-09-12T03:21:45.123Z초 단위만 필요하면:
- const now = new Date().toISOString().split(".")[0];
+ const now = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");📝 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.
| export const getRemindTime = async () => { | |
| const now = new Date().toISOString().split(".")[0]; | |
| const response = await apiRequest.get("/api/v1/users/remind-time", { | |
| params: { now }, | |
| }); | |
| return response.data; | |
| }; | |
| export const getRemindTime = async () => { | |
| const now = new Date().toISOString(); // 예: 2025-09-12T03:21:45.123Z | |
| const response = await apiRequest.get("/api/v1/users/remind-time", { | |
| params: { now }, | |
| }); | |
| return response.data; | |
| }; |
🤖 Prompt for AI Agents
In apps/extension/src/apis/axios.ts around lines 40 to 48, the current now
formatting uses toISOString().split(".")[0] which strips the millisecond and
trailing 'Z' (UTC) and can cause the server to misinterpret the timestamp as
local time; replace that expression so the request sends either the full ISO
string with timezone (use Date.prototype.toISOString() without splitting) or, if
only seconds precision is required, send a UTC seconds value (e.g.,
Math.floor(Date.now() / 1000)) and update the params accordingly so the server
receives an unambiguous UTC timestamp.
| export const usePutArticle = () => { | ||
| return useMutation({ | ||
| mutationFn: ({ articleId, data }: { articleId: number; data: PutArticleRequest }) => | ||
| putArticle(articleId, data), | ||
| onSuccess: (data) => { | ||
| console.log("아티클 수정 성공:", data); | ||
| }, | ||
| onError: (error) => { | ||
| console.error("아티클 수정 실패:", error); | ||
| }, | ||
| }); | ||
| }; No newline at end of file |
There was a problem hiding this comment.
🛠️ Refactor suggestion
putArticle 성공 시 캐시 무효화 누락 + 콘솔 가드
수정 후 저장 상태 쿼리를 무효화하여 UI 동기화를 보장하세요.
export const usePutArticle = () => {
- return useMutation({
+ const queryClient = useQueryClient();
+ return useMutation({
mutationFn: ({ articleId, data }: { articleId: number; data: PutArticleRequest }) =>
putArticle(articleId, data),
onSuccess: (data) => {
- console.log("아티클 수정 성공:", data);
+ if (data?.url) {
+ queryClient.invalidateQueries({ queryKey: ["articleSaved", data.url] });
+ }
+ if (process.env.NODE_ENV !== "production") {
+ // eslint-disable-next-line no-console
+ console.log("아티클 수정 성공:", data);
+ }
},
onError: (error) => {
- console.error("아티클 수정 실패:", error);
+ if (process.env.NODE_ENV !== "production") {
+ // eslint-disable-next-line no-console
+ console.error("아티클 수정 실패:", error);
+ }
},
});
};Committable suggestion skipped: line range outside the PR's diff.
🧰 Tools
🪛 GitHub Check: lint
[warning] 69-69:
Unexpected console statement
[warning] 66-66:
Unexpected console statement
🤖 Prompt for AI Agents
In apps/extension/src/apis/query/queries.ts around lines 61 to 72, the
usePutArticle mutation currently logs directly to console and does not
invalidate related queries after a successful put; update onSuccess to call the
react-query (or trpc) queryClient.invalidateQueries for the article list/detail
keys (and any "articles" or "article-{id}" cache entries) to refresh UI, and
replace raw console.log/console.error with a guarded logger or conditional that
only logs in development (e.g., check __DEV__ or use the app's logger) so
production consoles aren't polluted.
jjangminii
left a comment
There was a problem hiding this comment.
플로우가 복잡해서 어려웠을텐데 고생하셨어요-!! 코멘트 몇개만 확인해주세요!!
| }, | ||
| }); | ||
|
|
||
| // localStorage.setItem("token", 'eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJwaW5iYWNrIiwiaWQiOiI4NjA1NTBiMS1kZDBhLTQyMjMtYjM4OS0wNTEwYWU3MmNkMzUiLCJzdWIiOiJBY2Nlc3NUb2tlbiIsImV4cCI6MTc1NzYyOTAyMn0.qm-zqkuG2rpLlbUKJd9lUdh-4SStittgzXiwBeUMzA6NuKh_aEJmgoVInhUU-VSFtTlXP8eO9Ivao5K29LCRJA'); |
There was a problem hiding this comment.
헙ㅂ테스트용 토큰이였는데,! 제거해두겠습니당
| export interface PostArticleRequest { | ||
| url: string; | ||
| categoryId: number; | ||
| memo?: string | null; | ||
| remindTime?: string | null; | ||
| } |
There was a problem hiding this comment.
이건 따로 타입 파일로 분리하는건 어떤가요??
| onSuccess: (data) => { | ||
| console.log("저장 성공:", data); | ||
| }, | ||
| onError: (error) => { | ||
| console.error("저장 실패:", error); |
There was a problem hiding this comment.
이 부분은 따로 호출하는 페이지에서 handle 처리하는건 어떤가요? 실패시 ui가 있다면 따로 처리해도 좋을것같아요-!
| const newCategory: Category = { | ||
| categoryId: res.data.categoryId, | ||
| categoryName: categoryTitle, | ||
| categoryColor: res.data.categoryColor ?? "#000000", |
There was a problem hiding this comment.
이건 카테고리 컬러 못받는 에러가 나면, 디폴트 컬러가 피그마상 따로 지정이 없어서 우선 토큰을 쓰기보다 임의 색상코드 지정을 해두었습니다! 이부분 내일 QA때 여쭤보겠습니다!
|
|
||
| const DuplicatePop = ({onLeftClick,onRightClick} : DuplicatePopProps) => { | ||
| return ( | ||
| <div className="bg-white-bg flex w-[26rem] cursor-pointer flex-col items-center justify-center rounded-[1.2rem] px-[1.6rem] py-[2.4rem] shadow-[0_0_32px_0_rgba(0,0,0,0.10)]"> |
There was a problem hiding this comment.
저희 shadow 토큰 지정된거 있을텐데 사용 안하신 이유가 있나요??
| setIsArticleId(savedData.id ?? 0); | ||
|
|
||
| if (savedData.remindAt) { | ||
| const [rawDate, rawTime] = savedData.remindAt.split("T"); |
There was a problem hiding this comment.
단지 궁금해서 그런데 이건 어떤 코드일까요??
There was a problem hiding this comment.
과거에 저장했던 아티클 리마인드 시간을 받아서 이걸 팝업 데이트타임(공컴)에 깔아주는 로직인데요!
이게 서버에서 주는 savedData.remindAt 시간이 2025.08.19T14:00:00이런식으로 서버에서 데이터가 날라와서! 프론트에서 필요한 데이터 형식으로 가공하기 위해서 T기준으로 split(나누고) 프론트에서 쓸 2025.08.19 이랑 14:00 형태로 setState하는 로직입니당
| onRightClick={saveHandleCategory} | ||
| /> | ||
| )} | ||
| <div className="flex flex-col justify-between gap-[1.6rem] rounded-[12px] bg-white px-[3.2rem] py-[2.4rem] text-black"> |
There was a problem hiding this comment.
엇 네네! 이부분은 익스텐션 퍼블리싱 PR당시에, 진혁님이 rounded은 rem이 아니라, px로 하는 방향으로 피드백 주셔서 px로 적용해두었습니당!!
There was a problem hiding this comment.
rounded는 바뀌는 경우가 없기 때문에 rem대신 px을 써야할 거 같아요!
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (4)
apps/extension/src/pages/MainPop.tsx (4)
58-61: 저장 중 중복 클릭 방지 플래그 추가저장 중 버튼 비활성화를 위한 saving 상태를 두세요.
적용 diff:
const [isRemindOn, setIsRemindOn] = useState(false); const [memo, setMemo] = useState(''); + const [saving, setSaving] = useState(false); const [isPopupOpen, setIsPopupOpen] = useState(false);
26-27: mutate → mutateAsync 전환 및 비동기 제어에러 핸들링/버튼 비활성화 제어를 위해 mutateAsync 사용을 권장합니다.
적용 diff:
- const {mutate:postArticle} = usePostArticle(); - const {mutate:putArticle} = usePutArticle(); + const { mutateAsync: postArticle } = usePostArticle(); + const { mutateAsync: putArticle } = usePutArticle();
305-307: 저장 버튼 비활성화 조건 추가검증 에러/로딩/저장 중 중복 제출을 막으세요.
적용 diff:
- <Button size="medium" onClick={handleSave}> + <Button + size="medium" + onClick={handleSave} + disabled={loading || saving || (isRemindOn && (!!dateError || !!timeError))} + >
149-206: 저장 가드(검증) + 일관된 비동기 흐름(에러 처리/중복 클릭 방지) 필요
- 리마인드 ON인데 date/time 에러가 있어도 저장됨.
- 서버/로컬 저장을 기다리지 않아 실패 UX가 불명확.
적용 diff(핵심 부분만):
- const handleSave = async () => { - const currentDate = date; - const currentTime = time; - if (!selected || parseInt(selected) === 0) { - alert("카테고리를 선택해주세요!"); - return; - } + const handleSave = async () => { + if (loading || !url) { + alert("페이지 정보를 불러오는 중입니다. 잠시 후 다시 시도해주세요."); + return; + } + if (!selected /* TODO: 기본 카테고리 정책에 따라 0 허용 여부 확인 */) { + alert("카테고리를 선택해주세요!"); + return; + } + if (isRemindOn) { + const de = validateDate(date); + const te = validateTime(time); + setDateError(de); + setTimeError(te); + if (de || te) { + alert("리마인드 날짜/시간을 올바르게 입력해주세요."); + return; + } + } + const currentDate = date; + const currentTime = time; @@ - if (type === "add"){ - save({ + if (type === "add") { + try { + setSaving(true); + const payload = { + url, + categoryId: saveData.selectedCategory ? parseInt(saveData.selectedCategory) : 0, + memo: saveData.memo ?? "", + remindTime: combineDateTime(saveData.date ?? "", saveData.time ?? ""), + }; + await postArticle(payload); + await save({ url, title, description, imgUrl, memo, isRemindOn, selectedCategory: selected, - date: isRemindOn ? currentDate : date, - time: isRemindOn ? currentTime : time, - }); - postArticle( - { - url, - categoryId: saveData.selectedCategory - ? parseInt(saveData.selectedCategory) - : 0, - memo: saveData.memo, - remindTime: combineDateTime(saveData.date ?? "", saveData.time ?? ""), - } - ); - } else{ + date: isRemindOn ? currentDate : date, + time: isRemindOn ? currentTime : time, + }); + // TODO: 성공 UX (토스트/창 닫기) + } catch (_e) { + alert("저장에 실패했습니다. 잠시 후 다시 시도해주세요."); + } finally { + setSaving(false); + } + } else { - putArticle({ - articleId: isArticleId, - data: { - categoryId: saveData.selectedCategory - ? parseInt(saveData.selectedCategory) - : 0, - memo: saveData.memo, - now: new Date().toISOString(), - remindTime: combineDateTime(saveData.date ?? "", saveData.time ?? ""), - } - - }); + try { + setSaving(true); + await putArticle({ + articleId: isArticleId, + data: { + categoryId: saveData.selectedCategory ? parseInt(saveData.selectedCategory) : 0, + memo: saveData.memo ?? "", + now: new Date().toISOString(), + remindTime: combineDateTime(saveData.date ?? "", saveData.time ?? ""), + }, + }); + // TODO: 성공 UX (토스트/창 닫기) + } catch (_e) { + alert("수정에 실패했습니다. 잠시 후 다시 시도해주세요."); + } finally { + setSaving(false); + } }
🧹 Nitpick comments (5)
apps/extension/src/hooks/useSaveBookmarks.ts (1)
43-43: TODO 처리 위치 명확화성공/실패 흐름에 따라 창 닫기 시점을 명확히 하세요(예: 북마크 생성/서버 저장 모두 성공 시). 별도 이슈로 트래킹을 권장합니다.
원하시면 성공/실패 토스트 + 창 닫기 타이밍을 반영한 구현 패치를 제안드릴게요.
apps/extension/src/pages/MainPop.tsx (4)
112-116: 카테고리 인덱싱 안전화옵션 정렬/비동기 타이밍에 대비해 안전한 인덱싱을 사용하세요.
적용 diff:
- const handleSelect = (value: string | null, idx: number) => { - const categoryId = categoryData?.data?.categories[idx]?.categoryId.toString() ?? null; - setSelected(categoryId); - setSelectedCategoryName(value); - }; + const handleSelect = (value: string | null, idx: number) => { + const category = categoryData?.data?.categories?.[idx]; + if (!category) return; + setSelected(category.categoryId.toString()); + setSelectedCategoryName(value ?? category.categoryName); + };
123-129: useEffect 의존성 누락(type)lint 경고 해결: type을 의존성에 포함하세요.
적용 diff:
- }, [remindData]); + }, [type, remindData?.data]);
49-54: 이미지 기본값 처리 간소화(가독성)동일 동작을 더 간단히 표현할 수 있습니다.
적용 diff:
- if (!initialImgUrl) { - setImgUrl("https://thumb.photo-ac.com/31/3137071c02f608edb5220129b10533d6_t.jpeg"); - } else { - setImgUrl(initialImgUrl); - } + setImgUrl( + initialImgUrl || + "https://thumb.photo-ac.com/31/3137071c02f608edb5220129b10533d6_t.jpeg" + );
275-294: DateTime에 동적 key 부여는 불필요하고 포커스 손실을 유발할 수 있음key로 강제 리마운트하면 입력 포커스가 튈 수 있습니다. 제거를 권장합니다.
적용 diff:
- <DateTime - type="date" - key={`date-${date}`} + <DateTime + type="date" @@ - <DateTime - type="time" - key={`time-${time}`} + <DateTime + type="time"
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
apps/extension/src/apis/query/queries.ts(1 hunks)apps/extension/src/hooks/useSaveBookmarks.ts(1 hunks)apps/extension/src/pages/MainPop.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/extension/src/apis/query/queries.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-17T09:18:13.818Z
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#102
File: apps/extension/src/components/modalPop/ModalPop.tsx:166-172
Timestamp: 2025-07-17T09:18:13.818Z
Learning: In apps/extension/src/components/modalPop/ModalPop.tsx, the categories array should include "안 읽은 정보" (Unread Information) as the first default category that cannot be deleted. This default category is used consistently across the client-side dashboard and should be protected from deletion in the extension as well.
Applied to files:
apps/extension/src/pages/MainPop.tsx
🧬 Code graph analysis (1)
apps/extension/src/pages/MainPop.tsx (9)
apps/extension/src/types/types.ts (1)
ArticleResponse(7-14)apps/extension/src/apis/axios.ts (2)
postArticle(9-12)putArticle(65-68)apps/extension/src/apis/query/queries.ts (4)
usePostArticle(4-8)usePutArticle(43-48)useGetCategoriesExtension(21-26)useGetRemindTime(28-33)apps/extension/src/hooks/usePageMeta.ts (1)
usePageMeta(21-61)apps/extension/src/hooks/useSaveBookmarks.ts (1)
useSaveBookmark(13-50)apps/extension/src/utils/remindTimeFormat.ts (3)
updateDate(2-5)updateTime(8-11)combineDateTime(29-37)apps/extension/src/hooks/useCategoryManager.ts (1)
useCategoryManager(6-58)packages/design-system/src/components/dateTime/utils/ValidateData.ts (2)
validateDate(2-28)validateTime(31-48)packages/design-system/src/components/dateTime/DateTime.tsx (1)
DateTime(55-104)
🪛 GitHub Check: lint
apps/extension/src/hooks/useSaveBookmarks.ts
[warning] 39-39:
Unexpected console statement
apps/extension/src/pages/MainPop.tsx
[warning] 129-129:
React Hook useEffect has a missing dependency: 'type'. Either include it or remove the dependency array
🪛 GitHub Actions: ci
apps/extension/src/hooks/useSaveBookmarks.ts
[error] 38-38: TS6133: 'newBookmark' is declared but its value is never read.
🔇 Additional comments (1)
apps/extension/src/pages/MainPop.tsx (1)
152-155: 기본 카테고리(ID=0) 허용 여부 확인 필요apps/extension/src/pages/MainPop.tsx(152-155)에서 selected가 없거나 parseInt(selected) === 0이면 선택을 막고 있습니다. 코드 검색 결과 apps/extension/src/apis/axios.ts의 PostArticleRequest/PutArticleRequest에는 categoryId: number만 정의되어 있고 '안 읽은 정보' 또는 ID=0 사용 흔적은 없습니다. 서버 API 스펙과 팀 룰(‘안 읽은 정보’ 기본 카테고리 ID)을 확인한 후, 0을 허용할지 여부에 따라 해당 체크를 제거하거나 요청 로직을 조정하세요.
There was a problem hiding this comment.
CI 실패 원인 제거: 미사용 매개변수(newBookmark)와 console 로그 삭제
TS6133와 lint(no-console)를 함께 해결하세요.
적용 diff:
- (newBookmark) => {
- console.log('크롬 북마크바에 저장 완료');
- }
+ () => {}📝 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.
| (newBookmark) => { | |
| console.log('크롬 북마크바에 저장 완료:', newBookmark); | |
| console.log('크롬 북마크바에 저장 완료'); | |
| } | |
| ); | |
| () => {} | |
| ); |
🧰 Tools
🪛 GitHub Check: lint
[warning] 39-39:
Unexpected console statement
[warning] 38-38:
'newBookmark' is defined but never used
🪛 GitHub Actions: ci
[error] 38-38: TS6133: 'newBookmark' is declared but its value is never read.
🤖 Prompt for AI Agents
In apps/extension/src/hooks/useSaveBookmarks.ts around lines 38 to 41, remove
the unused parameter "newBookmark" from the arrow function and eliminate the
console.log call to resolve TS6133 and lint no-console; if you need to keep the
callback signature for API compatibility, rename the parameter to "_newBookmark"
(or prefix with an underscore) to mark it as intentionally unused and replace
console.log with a proper logger or remove the log entirely.
| putArticle({ | ||
| articleId: isArticleId, | ||
| data: { | ||
| categoryId: saveData.selectedCategory | ||
| ? parseInt(saveData.selectedCategory) | ||
| : 0, | ||
| memo: saveData.memo, | ||
| now: new Date().toISOString(), | ||
| remindTime: combineDateTime(saveData.date ?? "", saveData.time ?? ""), | ||
| } | ||
|
|
||
| }); |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
PUT 페이로드 now/remindTime 포맷 계약 재확인
서버 스펙이 now(초 단위까지만/밀리초 포함 여부)와 remindTime(null 허용)을 어떻게 기대하는지 검증이 필요합니다. 현재는 ISO 밀리초 포함, remindTime은 string|null입니다.
검증 스크립트:
원한다면 결과에 맞춰 포맷(예: toISOString().split('.')[0]) 보정 패치도 제공하겠습니다.
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -nP "interface\\s+PutArticleRequest|type\\s+PutArticleRequest" -C3 apps/extension/src || true
rg -nP "\\bnow\\b|remindTime" -n -C2 apps/extension/src || trueLength of output: 3525
PUT 페이로드 now/remindTime 포맷 불일치 — 수정 필요
- 근거: apps/extension/src/apis/axios.ts:41 — getRemindTime는 now를 new Date().toISOString().split('.')[0] (밀리초 제거)로 생성.
- 문제: apps/extension/src/pages/MainPop.tsx:199 — PUT 페이로드 now를 new Date().toISOString()로(밀리초 포함) 전송. apps/extension/src/pages/MainPop.tsx:200 — remindTime을 combineDateTime(...)으로 전송(비어있으면 빈 문자열 가능).
- 타입: apps/extension/src/apis/axios.ts:58-63 — PutArticleRequest는 now: string, remindTime: string | null (remindTime null 허용). 파일 상단에 유사 인터페이스들에서 memo/remindTime의 옵션성(?, null 허용)이 달라 중복·불일치 존재.
- 조치: 서버가 밀리초 포함을 허용하는지 확인하라. 허용하지 않으면 MainPop.tsx에서 now를 toISOString().split('.')[0]로 통일하고, remindTime이 비어있을 때는 빈 문자열 대신 null로 전달하며 PutArticleRequest 타입/주석을 일관되게 정리하라.
🤖 Prompt for AI Agents
In apps/extension/src/pages/MainPop.tsx around lines 192–203, the PUT payload
uses now with full ISO including milliseconds and sends remindTime as an empty
string when absent; update the payload to use now = new
Date().toISOString().split('.')[0] (remove milliseconds) and set remindTime to
null when combineDateTime(...) returns an empty value, and then align the
PutArticleRequest type in apps/extension/src/apis/axios.ts so remindTime is
string | null and now matches the trimmed ISO format; also run a quick check
against the server API to confirm it accepts milliseconds if you consider
leaving them, otherwise keep the trimmed format and make the interface and any
related memo/remindTime optionality consistent across files.
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/extension/src/hooks/useSaveBookmarks.ts (2)
21-24: chrome.storage 콜백 lastError 미처리로 인한 침묵 실패 가능성get/set 모두 lastError 체크가 없어 실패 시에도 정상 흐름처럼 진행됩니다. reject 처리 추가를 권장합니다.
- const result = await new Promise<{ bookmarks?: any[] }>((resolve) => { - chrome.storage.local.get(['bookmarks'], (items) => resolve(items)); - }); + const result = await new Promise<{ bookmarks?: any[] }>((resolve, reject) => { + chrome.storage.local.get(['bookmarks'], (items) => { + const err = chrome.runtime.lastError; + if (err) return reject(new Error(err.message)); + resolve(items); + }); + }); - await new Promise<void>((resolve) => { - chrome.storage.local.set({ bookmarks }, resolve); - }); + await new Promise<void>((resolve, reject) => { + chrome.storage.local.set({ bookmarks }, () => { + const err = chrome.runtime.lastError; + if (err) return reject(new Error(err.message)); + resolve(); + }); + });Also applies to: 28-31
13-50: 조치: useSaveBookmark 훅은 여전히 사용 중 — 서버 저장 흐름과의 책임 중복 정리 필요
- 사용처: apps/extension/src/pages/MainPop.tsx (import: line 14, 호출: line 35).
- 훅 구현: apps/extension/src/hooks/useSaveBookmarks.ts (약 lines 13–46) — chrome.storage.local.get/set 및 chrome.bookmarks.create 호출.
- 요구: 저장 책임을 설계대로 통일. 서버(post/put)만 사용할 계획이면 훅에서 chrome.storage/chrome.bookmarks 관련 코드를 제거하거나 훅을 서버 전용 어댑터로 변경; 로컬/북마크를 병행할 경우 서버와의 중복·동기화·에러/트랜잭션 처리를 명시해 구현 및 MainPop 호출 위치 조정.
- 추가: extension 전반에 console.* 로그(예: background.ts, content.ts, utils/OGFetch.ts, popup.tsx, apis/axiosInstance.ts) 존재 — 불필요한 로그 제거 또는 로깅 레벨 정리 필요.
♻️ Duplicate comments (1)
apps/extension/src/hooks/useSaveBookmarks.ts (1)
32-41: console 사용 제거 + 북마크 생성 콜백 Promise화 및 에러 처리 추가no-console 린트 경고(라인 39) 재발생. 또한 chrome.bookmarks.create 성공/실패 결과가 처리되지 않습니다. 콜백 파라미터(newBookmark)도 불필요합니다. 아래처럼 Promise로 감싸고 lastError를 체크하면 린트/안정성 모두 해결됩니다.
- chrome.bookmarks.create( - { - parentId: '1', - title: params.title || params.url, - url: params.url, - }, - (newBookmark) => { - console.log('크롬 북마크바에 저장 완료: ', newBookmark); - } - ); + await new Promise<void>((resolve, reject) => { + chrome.bookmarks.create( + { + parentId: '1', + title: params.title || params.url, + url: params.url, + }, + () => { + const err = chrome.runtime.lastError; + if (err) return reject(new Error(err.message)); + resolve(); + } + ); + });
🧹 Nitpick comments (1)
apps/extension/src/hooks/useSaveBookmarks.ts (1)
43-43: TODO 정리: 팝업 자동 종료 정책 확정 필요주 흐름(DuplicatePop/MainPop)에서 UX가 결정된다면 이 위치의 TODO는 제거하거나 이슈 번호로 연결하세요. 자동 종료를 쓸 경우엔 위의 비동기 작업 성공 이후에만 호출되도록 배치하세요.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/extension/src/hooks/useSaveBookmarks.ts(1 hunks)
🧰 Additional context used
🪛 GitHub Check: lint
apps/extension/src/hooks/useSaveBookmarks.ts
[warning] 39-39:
Unexpected console statement
constantly-dev
left a comment
There was a problem hiding this comment.
확실히 form 관리할 것도 많고 분기가 많아서 복잡하네요.. 고생하셨습니다!
너무 복잡하면 react hook form등 라이브러리를 도입하거나 내부 코드를 참고해서 리팩토링해도 좋을 것 같아요 👍
| <> | ||
| {isDuplicatePop ? ( | ||
| <DuplicatePop | ||
| onLeftClick={handleDuplicateLeftClick} | ||
| onRightClick={handleDuplicateRightClick} | ||
| /> | ||
| ) : ( | ||
| <MainPop type={mainPopType} savedData={isSaved?.data}/> | ||
| )} | ||
| </> |
There was a problem hiding this comment.
컴포넌트를 분리해주셔서 app이 너무 깔끔해졌어요!!! 👍
| export const getCategoriesExtension = async () => { | ||
| const response = await apiRequest.get("/api/v1/categories/extension"); | ||
| return response.data; | ||
| }; |
There was a problem hiding this comment.
| export const getCategoriesExtension = async () => { | |
| const response = await apiRequest.get("/api/v1/categories/extension"); | |
| return response.data; | |
| }; | |
| export const getCategoriesExtension = async () => { | |
| const { data } = await apiRequest.get("/api/v1/categories/extension"); | |
| return data; | |
| }; |
바로 구조 분해 할당도 가능합니다!!
| export const useGetArticleSaved = (url:string) => { | ||
| return useQuery({ | ||
| queryKey: ["articleSaved", url], | ||
| queryFn: () => getArticleSaved(url), | ||
| enabled: !!url, | ||
| }); | ||
| } |
There was a problem hiding this comment.
넵네!! 그래서 enabled처리해두었습니당
| const resetPopup = () => { | ||
| setCategoryTitle(""); | ||
| setIsPopError(false); | ||
| setErrorTxt(""); | ||
| }; |
| onError: (err: AxiosError<{ code: string; message: string }>) => { | ||
| alert(err.response?.data?.message ?? "카테고리 추가 중 오류가 발생했어요 😢"); | ||
| }, |
There was a problem hiding this comment.
요렇게 해당 부분에서 타입을 지정해도 되지만, useQuery나 useMutation사용한 쿼리 함수 작성 부분에서 미리 타입을 정의해주면 이 부분에서 쓰지 않아도 돼요!! 예를 들어
쿼리 결과 값을 타입 지정할 수 있게 tanstack query에서 타입을 제공해주는데 UseQueryResult, UseMutationResult를 사용하면 돼요!
제네릭에 첫 번째 값은 success에서 받는 response의 타입, 두 번째는 error의 타입! 그래서 두 번째에는 저희가 axios를 해서 받기때문에 AxiosError라고 타입 지정을 여기서 해줄 수 있어요!
예시 코드)
export const useGetBookmarkArticles = (
page: number,
size: number
): UseQueryResult<BookmarkArticleResponse, AxiosError> => {
return useQuery({
queryKey: ['bookmarkReadArticles', page, size],
queryFn: () => getBookmarkArticles(page, size),
});
};리팩토링 할 때 더 공부해서 한번에 해도 될 것 같아요 👍 참고 정도로!
There was a problem hiding this comment.
오오 감사합니다!! usemutaion, useQuery를 사용한다면 이 쿼리함수 기능 등을 더 잘 공부해서 적용해봐야겠어요.!
| <InfoBox | ||
| title={title || '제목 로딩 중...'} | ||
| source={description || '불러오는 중입니다'} | ||
| imgUrl={imgUrl} | ||
| /> |
There was a problem hiding this comment.
해당 부분 한 data라도 없으면 infoBox영역을 전체 skeleton ui로 보여주는 등의 UX처리를 해주면 좋겠네요!
QA 이후에 다시 한번 얘기해봐요 👍
There was a problem hiding this comment.
skeleton ui 저희 서비스에 분명 필요할 것 같아서! qa때 요청하면 좋겠네용 굿굿
| onRightClick={saveHandleCategory} | ||
| /> | ||
| )} | ||
| <div className="flex flex-col justify-between gap-[1.6rem] rounded-[12px] bg-white px-[3.2rem] py-[2.4rem] text-black"> |
There was a problem hiding this comment.
rounded는 바뀌는 경우가 없기 때문에 rem대신 px을 써야할 거 같아요!
| useEffect(() => { | ||
| if (!initialImgUrl) { | ||
| setImgUrl("https://thumb.photo-ac.com/31/3137071c02f608edb5220129b10533d6_t.jpeg"); | ||
| } else { | ||
| setImgUrl(initialImgUrl); | ||
| } | ||
| }, [initialImgUrl]); |
There was a problem hiding this comment.
요거는 아래 이미지 지정해주는 부분에서 || 로 fallback 처리해주면 되지 않을까요? useEffect의 필요성에 대해서 질문드립니다!
const defaultImageUrl = "https://thumb.photo-ac.com/31/3137071c02f608edb5220129b10533d6_t.jpeg";
<InfoBox
title={title || '제목 로딩 중...'}
source={description || '불러오는 중입니다'}
imgUrl={initialImgUrl || defaultImageUrl}
/>
잘 해결해주셨네요!! react hook form에서도 validation trigger를 onBlur나 onChange등으로 커스텀 할 수 있는데 👍 |
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (4)
apps/extension/src/pages/MainPop.tsx (4)
26-27: mutate → mutateAsync로 전환하고 비동기 흐름을 await로 제어하세요에러 처리·버튼 비활성화 타이밍 제어를 위해 Promise를 반환하는 mutateAsync 사용이 적합합니다.
- const {mutate:postArticle} = usePostArticle(); - const {mutate:putArticle} = usePutArticle(); + const { mutateAsync: postArticle } = usePostArticle(); + const { mutateAsync: putArticle } = usePutArticle();
59-63: 저장 중 상태(saving) 도입 및 버튼 비활성화사용자 다중 클릭/중복 요청을 방지하세요.
const [isRemindOn, setIsRemindOn] = useState(false); const [memo, setMemo] = useState(''); const [isPopupOpen, setIsPopupOpen] = useState(false); const [isArticleId, setIsArticleId] = useState(0); + const [saving, setSaving] = useState(false);- <Button size="medium" onClick={handleSave}> + <Button + size="medium" + onClick={handleSave} + disabled={loading || saving || !selected || (isRemindOn && (!!dateError || !!timeError))} + > 저장 </Button>Also applies to: 307-309
150-170: 리마인드 유효성 오류 상태에서도 저장이 진행됩니다isRemindOn인 경우 date/time 검증 후 오류 시 저장을 차단하세요. url 미로딩/부재도 가드 필요합니다.
const handleSave = async () => { const currentDate = date; const currentTime = time; + if (loading || !url) { + alert("페이지 정보를 불러오는 중입니다. 잠시 후 다시 시도해주세요."); + return; + } if (!selected || parseInt(selected) === 0) { alert("카테고리를 선택해주세요!"); return; } + if (isRemindOn) { + const de = validateDate(currentDate); + const te = validateTime(currentTime); + setDateError(de); + setTimeError(te); + if (de || te) { + alert("리마인드 날짜/시간을 올바르게 입력해주세요."); + return; + } + }
171-192: 비동기 흐름: 에러 처리/순서 제어 없이 save와 API 호출이 동시 실행됩니다await + try/catch로 직렬화하고 saving 상태를 반영하세요. mutateAsync 전환 전제.
- if (type === "add"){ - save({ + if (type === "add"){ + try { + setSaving(true); + // 서버 저장 성공 보장 후 로컬 저장(필요 시 순서 조정 가능) + await postArticle({ + url, + categoryId: selected ? parseInt(selected, 10) : 0, + memo, + remindTime: combineDateTime( + isRemindOn ? currentDate : "", + isRemindOn ? currentTime : "" + ), + }); + await save({ url, title, description, - imgUrl, + imgUrl, memo, isRemindOn, selectedCategory: selected, - date: isRemindOn ? currentDate : date, - time: isRemindOn ? currentTime : time, - }); - postArticle( - { - url, - categoryId: saveData.selectedCategory - ? parseInt(saveData.selectedCategory) - : 0, - memo: saveData.memo, - remindTime: combineDateTime(saveData.date ?? "", saveData.time ?? ""), - } - ); - } else{ - putArticle({ - articleId: isArticleId, - data: { - categoryId: saveData.selectedCategory - ? parseInt(saveData.selectedCategory) - : 0, - memo: saveData.memo, - now: new Date().toISOString(), - remindTime: combineDateTime(saveData.date ?? "", saveData.time ?? ""), - } - - }); - } + date: isRemindOn ? currentDate : "", + time: isRemindOn ? currentTime : "", + }); + } catch (e) { + alert("저장에 실패했습니다. 잠시 후 다시 시도해주세요."); + } finally { + setSaving(false); + } + } else{ + try { + setSaving(true); + await putArticle({ + articleId: isArticleId, + data: { + categoryId: selected ? parseInt(selected, 10) : 0, + memo, + now: new Date().toISOString(), // now 포맷은 아래 코멘트 참고 + remindTime: combineDateTime( + isRemindOn ? currentDate : "", + isRemindOn ? currentTime : "" + ), + } + }); + } catch (e) { + alert("수정에 실패했습니다. 잠시 후 다시 시도해주세요."); + } finally { + setSaving(false); + } + }Also applies to: 194-206
🧹 Nitpick comments (6)
apps/extension/src/pages/MainPop.tsx (6)
124-131: useEffect 의존성 누락 및 느슨한 비교type을 의존성에 포함하고, === 사용이 안전합니다.
- useEffect(() => { - if (remindData?.data && type=='add') { + useEffect(() => { + if (remindData?.data && type === 'add') { const newDate = updateDate(remindData.data.remindDate); const newTime = updateTime(remindData.data.remindTime); setDate(newDate); setTime(newTime); } - }, [remindData]); + }, [remindData, type]);
94-103: 기본 카테고리 “안 읽은 정보” 존재/고정 보장 필요팀 룰상 첫 번째 기본 카테고리를 삭제 불가로 유지해야 합니다(7/17 학습). useCategoryManager 옵션 생성/추가 시 이를 강제하는지 확인 부탁드립니다. 미흡 시 훅에서 예약어 차단·옵션 보정·쿼리 무효화까지 포함해 보완하겠습니다.
36-56: 이미지 fallback 로직 이중화 — 단일 소스로 단순화state+useEffect와 렌더시 || fallback이 공존합니다. 한 가지 방식으로 통일하세요. 아래처럼 상수로 계산해 재사용하면 됩니다.
- const [imgUrl, setImgUrl] = useState(initialImgUrl); + const imgUrl = initialImgUrl || defaultImageUrl;- useEffect(() => { - if (!initialImgUrl) { - setImgUrl(defaultImageUrl); - } else { - setImgUrl(initialImgUrl); - } - }, [initialImgUrl]);- <InfoBox - title={title || '제목 로딩 중...'} - source={description || '불러오는 중입니다'} - imgUrl={initialImgUrl || defaultImageUrl} - /> + <InfoBox + title={title || '제목 로딩 중...'} + source={description || '불러오는 중입니다'} + imgUrl={imgUrl} + />Also applies to: 240-244
299-303: 리마인드 OFF 상태에선 에러 메시지 숨기기사용자 혼란을 줄이기 위해 isRemindOn일 때만 에러를 노출하세요.
- {dateError ? ( + {isRemindOn && (dateError ? ( <p className="body3-r text-error">{dateError}</p> ) : timeError ? ( <p className="body3-r text-error">{timeError}</p> - ) : null} + ) : null)}
154-155: parseInt에 radix 명시예기치 않은 해석을 피하려면 10진수 명시가 안전합니다.
- if (!selected || parseInt(selected) === 0) { + if (!selected || parseInt(selected, 10) === 0) {- categoryId: saveData.selectedCategory - ? parseInt(saveData.selectedCategory) + categoryId: saveData.selectedCategory + ? parseInt(saveData.selectedCategory, 10) : 0,- categoryId: saveData.selectedCategory - ? parseInt(saveData.selectedCategory) + categoryId: saveData.selectedCategory + ? parseInt(saveData.selectedCategory, 10) : 0,Also applies to: 186-188, 197-199
158-169: saveData.createdAt는 실제 저장 경로에서 미사용useSaveBookmark가 createdAt를 자체 생성하므로 이 필드는 혼란만 유발합니다. 제거 또는 실제 사용처와 일치시키세요.
const saveData = { url, title, description, imgUrl, memo, isRemindOn, selectedCategory: selected, date: isRemindOn ? currentDate : date, time: isRemindOn ? currentTime : time, - createdAt: new Date().toISOString(), };
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/extension/src/pages/DuplicatePop.tsx(1 hunks)apps/extension/src/pages/MainPop.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/extension/src/pages/DuplicatePop.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-17T09:18:13.818Z
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#102
File: apps/extension/src/components/modalPop/ModalPop.tsx:166-172
Timestamp: 2025-07-17T09:18:13.818Z
Learning: In apps/extension/src/components/modalPop/ModalPop.tsx, the categories array should include "안 읽은 정보" (Unread Information) as the first default category that cannot be deleted. This default category is used consistently across the client-side dashboard and should be protected from deletion in the extension as well.
Applied to files:
apps/extension/src/pages/MainPop.tsx
🧬 Code graph analysis (1)
apps/extension/src/pages/MainPop.tsx (9)
apps/extension/src/types/types.ts (1)
ArticleResponse(7-14)apps/extension/src/apis/axios.ts (2)
postArticle(9-12)putArticle(65-68)apps/extension/src/apis/query/queries.ts (4)
usePostArticle(4-8)usePutArticle(43-48)useGetCategoriesExtension(21-26)useGetRemindTime(28-33)apps/extension/src/hooks/usePageMeta.ts (1)
usePageMeta(21-61)apps/extension/src/hooks/useSaveBookmarks.ts (1)
useSaveBookmark(13-50)apps/extension/src/utils/remindTimeFormat.ts (3)
updateDate(2-5)updateTime(8-11)combineDateTime(29-37)apps/extension/src/hooks/useCategoryManager.ts (1)
useCategoryManager(6-58)packages/design-system/src/components/dateTime/utils/ValidateData.ts (2)
validateDate(2-28)validateTime(31-48)packages/design-system/src/components/dateTime/DateTime.tsx (1)
DateTime(55-104)
🪛 GitHub Check: lint
apps/extension/src/pages/MainPop.tsx
[warning] 131-131:
React Hook useEffect has a missing dependency: 'type'. Either include it or remove the dependency array
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: storybook
🔇 Additional comments (2)
apps/extension/src/pages/MainPop.tsx (2)
201-203: PUT payload의 now 포맷 일관화 필요 (밀리초 포함 여부)서버가 초 단위까지만 허용한다면 toISOString().split('.')[0]로 통일하세요. 현재 getRemindTime 쪽 포맷과 불일치 가능성이 있습니다.
- now: new Date().toISOString(), + now: new Date().toISOString().split('.')[0],서버 사양이 밀리초 허용이면 무시해도 됩니다. 확인 부탁드립니다.
114-118: Dropdown onChange 인덱스 안전성 확인 및 가드 추가 권장idx가 -1/범위초과일 경우 방어로 early return 해주세요. DS 시그니처도 재확인 바랍니다.
const handleSelect = (value: string | null, idx: number) => { + if (idx == null || idx < 0) return; const categoryId = categoryData?.data?.categories[idx]?.categoryId.toString() ?? null; setSelected(categoryId); setSelectedCategoryName(value); };
📌 Related Issues
📄 Tasks
#⭐ PR Point (To Reviewer)
1. app.tsx 큰 틀 수정
익스텐션 팝업 ui는 동일하지만, type 역할이 달라요!!
[type : add] 초기 아티클 첫 저장!
[type : edit] 똑같은 아티클 저장하려하면?
이렇게 서로 기능이 달라서!! 중복 저장인지 분기 따라서, mainPop과 alertPop 페이지를 나눠서 관리중!
2. 데이트타임 공컴 : 포맷 및 값 반환 시점 수정
[문제1. 백스페이스 입력 시 커서 꼬임 문제]
입력 백스페이스 할때마다 커서 위치가 자꾸 포커스 나감
[문제2. 부모 state와 내부 표시 값 불일치]
time인 시간 값의 경우에는 실시간 포맷팅 때문에 onChange 기능을 꺼뒀었는데,, 그래서 그 부모 MainPop에서 이 수정된 time의 값을 받아오지 못하고 잘못된 값을 서버에 보내는 문제!
[문제3. UX 적인 문제]
사용자는 입력 중인데도 포맷팅된 값이 부모 단으로 넘어가면서
validation 에러 메시지가 너무 이른 타이밍에 떠서 사용성을 해치고 있었음..
🔧 해결!!
onBlur 시점에만 부모로 값 전달하도록 시점 수정!
입력 중에는 내부 state(rawDigits)에서만 관리하고, 실시간 포맷팅은 함
부모는 blur이벤트가 발생했을 때에만 최종 포맷팅된 값만 받도록 변경.
3. 카테고리 저장 및 추가 훅
useCategoeyManager훅으로 따로 빼서!! 카테고리 추가하려는 팝업 로직을 따로 리팩토링 분리했슴!
++ 드롭다운 카테고리 지정 시에, 바로 클릭 반영이 되어야 해서 드롭다운 공컴 구조 수정이 있었슴다!
📷 Screenshot
Summary by CodeRabbit
신규 기능
개선