Skip to content
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
18a9826
api: 익스텐션 저장 api 연동
jllee000 Sep 11, 2025
3513d8b
Merge remote-tracking branch 'origin/develop' into api/#67/extension-…
jllee000 Sep 11, 2025
2cb25a8
feat: 일시 조회 및 포맷팅 연결
jllee000 Sep 11, 2025
b125950
feat: duplicate 기준 분리
jllee000 Sep 11, 2025
ba5af17
feat: 수정 / 추가 분기 로직
jllee000 Sep 11, 2025
06a4e4d
feat: 실제 북마크 저장 위치 수정
jllee000 Sep 11, 2025
4a7febf
feat: 오늘 날짜데이터 전달
jllee000 Sep 11, 2025
3f0f135
feat: 카테고리 얼럿 추가
jllee000 Sep 11, 2025
c79473c
feat: 메타데이터 에러 분기
jllee000 Sep 11, 2025
735632d
chore: 주석 제거
jllee000 Sep 11, 2025
6f480d3
feat: 카테고리 연동 및 데이터 구조 수정
jllee000 Sep 11, 2025
8a031d6
chore: 머지 충돌 에러 수정
jllee000 Sep 11, 2025
00b0400
feat: api 서버 도메인 수정
jllee000 Sep 11, 2025
a836ee8
feat: 토큰 크롬스토리지 구조로 수정
jllee000 Sep 11, 2025
7280bbe
feat: 카테고리 추가 바로 반영 및 얼럿 추가
jllee000 Sep 12, 2025
d2bea02
feat: 서비스명 수정
jllee000 Sep 12, 2025
1ee1580
feat: 리마인드 타임 onChange 조작 수정
jllee000 Sep 12, 2025
b414207
feat: 실시간 포맷 복구 및 blur일때만 부모한테 전달
jllee000 Sep 12, 2025
474075f
Merge remote-tracking branch 'origin/develop' into api/#67/extension-…
jllee000 Sep 12, 2025
806986e
feat: 스토리북 코드 수정
jllee000 Sep 12, 2025
659a7e2
feat: 스토리북 빌드 에러
jllee000 Sep 12, 2025
20a50e8
feat: 주석 제거
jllee000 Sep 12, 2025
7564555
feat: 스토리북 기본 인풋 placeholder로 채우기
jllee000 Sep 12, 2025
ac9d9a6
feat: 임시 토큰 제거 및 api 파일 구조 수정
jllee000 Sep 12, 2025
d2dd838
feat: api 쿼리 분기 제거
jllee000 Sep 13, 2025
8ebed5b
feat: TODO 표시
jllee000 Sep 13, 2025
90002f3
feat: TODO 표시
jllee000 Sep 13, 2025
ea1746c
chore: 빌드에러 수정
jllee000 Sep 13, 2025
165805e
chore: 머지 에러충돌 해결
jllee000 Sep 13, 2025
9cd52cf
feat: 코드리뷰 반영 디폴트 썸네일 이미지
jllee000 Sep 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/extension/popup.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>PinBack Extension</title>
<title>PinBack</title>
</head>
<body>
<div id="root"></div>
Expand Down
194 changes: 29 additions & 165 deletions apps/extension/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,179 +1,43 @@
import './App.css';
import {
InfoBox,
Button,
Textarea,
DateTime,
Switch,
PopupContainer,
Dropdown,
validateDate,
validateTime
} from '@pinback/design-system/ui';
import { useState } from 'react';
import DuplicatePop from './pages/DuplicatePop';
import MainPop from './pages/MainPop';
import { useState, useEffect } from 'react';
import { useGetArticleSaved } from '@apis/query/queries';
import { usePageMeta } from './hooks/usePageMeta';
import { useSaveBookmark } from './hooks/useSaveBookmarks';
import { Icon } from '@pinback/design-system/icons';
const App = () => {
const [isRemindOn, setIsRemindOn] = useState(false);
const [memo, setMemo] = useState('');
const [isPopupOpen, setIsPopupOpen] = useState(false);

const [selected, setSelected] = useState<string | null>(null);
// 시간,날짜 검사 구간!
const [date, setDate] = useState('2025.10.10');
const [time, setTime] = useState('19:00');
const [dateError, setDateError] = useState('');
const [timeError, setTimeError] = useState('');
const App = () => {
const { url } = usePageMeta();
const { data: isSaved } = useGetArticleSaved(url);

const handleDateChange = (value: string) => {
setDate(value);
setDateError(validateDate(value));
};
const [isDuplicatePop, setIsDuplicatePop] = useState(false);
const [mainPopType, setMainPopType] = useState<"add" | "edit">("add");

const handleTimeChange = (value: string) => {
setTime(value);
setTimeError(validateTime(value));
};
useEffect(() => {
if (isSaved?.data) {
setIsDuplicatePop(true);
}
}, [isSaved]);
Comment on lines +12 to +19
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

재조회로 인한 DuplicatePop 재등장 방지(사용자 확인 상태 보존)

포커스/리패치 등으로 isSaved가 갱신되면 DuplicatePop이 다시 나타날 수 있습니다. 사용자가 한 번 확인하면 더 이상 토글되지 않도록 상태를 추가하세요.

   const [isDuplicatePop, setIsDuplicatePop] = useState(false);
   const [mainPopType, setMainPopType] = useState<"add" | "edit">("add");
+  const [dupAck, setDupAck] = useState(false); // 사용자가 중복 알림을 확인했는지

   useEffect(() => {
-    if (isSaved?.data) {
+    if (!dupAck && isSaved?.data) {
       setIsDuplicatePop(true);
     }
-  }, [isSaved]);
+  }, [dupAck, isSaved]);
 ...
   const handleDuplicateLeftClick = () => {
     setIsDuplicatePop(false);
     setMainPopType("edit");
+    setDupAck(true);
   };

Also applies to: 21-24

🤖 Prompt for AI Agents
In apps/extension/src/App.tsx around lines 12-19 (and similarly 21-24), the
DuplicatePop reopens whenever isSaved updates; add a new boolean state (e.g.,
hasConfirmedDuplicate) to persist that the user already acknowledged the
duplicate and update the useEffect(s) to set isDuplicatePop true only when
isSaved?.data is present AND hasConfirmedDuplicate is false; also ensure the
user confirmation handler sets hasConfirmedDuplicate true so future isSaved
changes do not reopen the popup.


// 스위치
const handleSwitchChange = (checked: boolean) => {
setIsRemindOn(checked);
const handleDuplicateLeftClick = () => {
setIsDuplicatePop(false);
setMainPopType("edit");
};

const { url, title, description, imgUrl } = usePageMeta();
const { save } = useSaveBookmark();

const handleSave = async () => {
save({
url,
title,
description,
imgUrl,
memo,
isRemindOn,
selectedCategory: selected,
date: isRemindOn ? date : null,
time: isRemindOn ? time : null,
});
const saveData = {
url,
title,
description,
imgUrl,
memo,
isRemindOn,
selectedCategory: selected,
date: isRemindOn ? date : null,
time: isRemindOn ? time : null,
createdAt: new Date().toISOString(),
};
console.log('저장된 데이터:', saveData);
const handleDuplicateRightClick = () => {
window.location.href = "/dashboard";
};
const [categoryTitle, setCategoryTitle] = useState('');
const [isPopError, setIsPopError] = useState(false);
const [errorTxt, setErrorTxt] = useState('');
const saveCategory = () => {
if (categoryTitle.length >20){
setIsPopError(true);
setErrorTxt('20자 이내로 작성해주세요');
} else{
setIsPopupOpen(false);
}
}


return (
<div className="App">
<div className="relative flex h-[56.8rem] w-[31.2rem] items-center justify-center">
{isPopupOpen && (
<PopupContainer
isOpen={isPopupOpen}
onClose={() => setIsPopupOpen(false)}
type="input"
title="카테고리 추가하기"
left="취소"
right="확인"
inputValue={categoryTitle}
isError={isPopError}
errortext={errorTxt}
onInputChange={setCategoryTitle}
placeholder="카테고리 제목을 입력해주세요"
onLeftClick={() => setIsPopupOpen(false)}
onRightClick={saveCategory}
/>
)}
<div className="flex flex-col justify-between gap-[1.6rem] rounded-[12px] bg-white px-[3.2rem] py-[2.4rem] text-black">
<div className="mr-auto">
<Icon name="main_logo" width={72} height={20} />
</div>

<InfoBox
title={title || '제목 없음'}
source={description || '웹페이지'}
imgUrl={imgUrl}
/>

<div>
<p className="caption1-sb mb-[0.4rem]">카테고리</p>
<Dropdown
options={['옵션1', '옵션2']}
selectedValue={selected}
onChange={(value) => setSelected(value)}
placeholder="선택해주세요"
onAddItem={() => setIsPopupOpen(true)}
addItemLabel="추가하기"
/>
</div>

<div>
<p className="caption1-sb mb-[0.4rem]">메모</p>
<Textarea
maxLength={100}
placeholder="나중에 내가 꺼내줄 수 있게 살짝 적어줘!"
onChange={(e) => setMemo(e.target.value)}
/>
</div>

<div>
<div className="mb-[0.4rem] flex items-center justify-between">
<p className="caption1-sb">리마인드</p>
<Switch
onCheckedChange={handleSwitchChange}
checked={isRemindOn}
/>
</div>

<div className="mb-[0.4rem] flex items-center justify-between gap-[0.8rem]">
<DateTime
type="date"
state={
dateError ? 'error' : isRemindOn ? 'default' : 'disabled'
}
value={date}
onChange={handleDateChange}
/>
<DateTime
type="time"
state={
timeError ? 'error' : isRemindOn ? 'default' : 'disabled'
}
value={time}
onChange={handleTimeChange}
/>
</div>

{/* 에러 메시지 출력 */}
{dateError && <p className="body3-r text-error">{dateError}</p>}
{timeError && <p className="body3-r text-error">{timeError}</p>}
</div>

<Button size="medium" onClick={handleSave}>
저장
</Button>
</div>
</div>
</div>
<>
{isDuplicatePop ? (
<DuplicatePop
onLeftClick={handleDuplicateLeftClick}
onRightClick={handleDuplicateRightClick}
/>
) : (
<MainPop type={mainPopType} savedData={isSaved?.data}/>
)}
</>
Comment on lines +31 to +40
Copy link
Member

Choose a reason for hiding this comment

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

컴포넌트를 분리해주셔서 app이 너무 깔끔해졌어요!!! 👍

);
};

Expand Down
68 changes: 68 additions & 0 deletions apps/extension/src/apis/axios.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import apiRequest from "./axiosInstance";
export interface PostArticleRequest {
url: string;
categoryId: number;
memo?: string | null;
remindTime?: string | null;
}

export const postArticle = async (data: PostArticleRequest) => {
const response = await apiRequest.post("/api/v1/articles", data);
return response.data;
};


export interface postSignupRequest {
email: string;
remindDefault: string
fcmToken: string;
}

export const postSignup = async (data: postSignupRequest) => {
const response = await apiRequest.post("/api/v1/auth/signup", data);
return response.data;
};

export const getCategoriesExtension = async () => {
const response = await apiRequest.get("/api/v1/categories/extension");
return response.data;
};
Comment on lines +26 to +29
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
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 interface postCategoriesRequest {
categoryName: string;
}

export const postCategories = async (data: postCategoriesRequest) => {
const response = await apiRequest.post("/api/v1/categories", data);
return response.data;
}

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;
};
Comment on lines +40 to +48
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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 getArticleSaved=async (url:string) => {
const response = await apiRequest.get("/api/v1/articles/saved", {
params: { url },
});
return response.data;
}

export interface PutArticleRequest {
categoryId: number;
memo: string;
now: string;
remindTime: string | null;
}

export const putArticle = async (articleId: number, data: PutArticleRequest) => {
const response = await apiRequest.put(`/api/v1/articles/${articleId}`, data);
return response.data;
};
48 changes: 48 additions & 0 deletions apps/extension/src/apis/query/queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useMutation,useQuery } from "@tanstack/react-query";
import { postArticle, PostArticleRequest,postSignup, postSignupRequest, getCategoriesExtension, postCategories, postCategoriesRequest, getRemindTime, getArticleSaved,putArticle, PutArticleRequest} from "@apis/axios";

export const usePostArticle = () => {
return useMutation({
mutationFn: (data: PostArticleRequest) => postArticle(data),
});
};
Comment on lines +4 to +8
Copy link

Choose a reason for hiding this comment

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

🛠️ 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.

Suggested change
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 usePostSignup = () => {
return useMutation({
mutationFn: (data: postSignupRequest) => postSignup(data)
});
}

export const usePostCategories = () => {
return useMutation({
mutationFn: (data: postCategoriesRequest) => postCategories(data),
});
}
Comment on lines +28 to +20
Copy link

Choose a reason for hiding this comment

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

🛠️ 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.

Suggested change
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 useGetCategoriesExtension = () => {
return useQuery({
queryKey: ["categoriesExtension"],
queryFn: getCategoriesExtension,
});
};

export const useGetRemindTime = () => {
return useQuery({
queryKey: ["remindTime"],
queryFn: getRemindTime,
});
}

export const useGetArticleSaved = (url:string) => {
return useQuery({
queryKey: ["articleSaved", url],
queryFn: () => getArticleSaved(url),
enabled: !!url,
});
}
Comment on lines +35 to +41
Copy link

Choose a reason for hiding this comment

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

🛠️ 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.

Suggested change
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.

Comment on lines +35 to +41
Copy link
Member

Choose a reason for hiding this comment

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

url이 있어야만 호출을 하는거죠??

Copy link
Collaborator Author

@jllee000 jllee000 Sep 13, 2025

Choose a reason for hiding this comment

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

넵네!! 그래서 enabled처리해두었습니당


export const usePutArticle = () => {
return useMutation({
mutationFn: ({ articleId, data }: { articleId: number; data: PutArticleRequest }) =>
putArticle(articleId, data)
});
};
Comment on lines +61 to +48
Copy link

Choose a reason for hiding this comment

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

🛠️ 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.

58 changes: 58 additions & 0 deletions apps/extension/src/hooks/useCategoryManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useState } from "react";
import { usePostCategories, useGetCategoriesExtension } from "@apis/query/queries";
import type { Category } from "@shared-types/types";
import { AxiosError } from "axios";

export const useCategoryManager = () => {
const { data: categoryData } = useGetCategoriesExtension();
const { mutate: postCategories } = usePostCategories();

const [categoryTitle, setCategoryTitle] = useState("");
const [isPopError, setIsPopError] = useState(false);
const [errorTxt, setErrorTxt] = useState("");

const options =
categoryData?.data?.categories?.map((c: Category) => c.categoryName) ?? [];

const saveCategory = (onSuccess?: (category: Category) => void) => {
if (categoryTitle.length > 20) {
setIsPopError(true);
setErrorTxt("20자 이내로 작성해주세요");
return;
}

postCategories(
{ categoryName: categoryTitle },
{
onSuccess: (res) => {
const newCategory: Category = {
categoryId: res.data.categoryId,
categoryName: categoryTitle,
categoryColor: res.data.categoryColor ?? "#000000",
Copy link
Collaborator

Choose a reason for hiding this comment

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

이건 왜 토큰지정 안된 컬러인가요??

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

이건 카테고리 컬러 못받는 에러가 나면, 디폴트 컬러가 피그마상 따로 지정이 없어서 우선 토큰을 쓰기보다 임의 색상코드 지정을 해두었습니다! 이부분 내일 QA때 여쭤보겠습니다!

};
onSuccess?.(newCategory);
resetPopup();
},
onError: (err: AxiosError<{ code: string; message: string }>) => {
alert(err.response?.data?.message ?? "카테고리 추가 중 오류가 발생했어요 😢");
},
Comment on lines +36 to +38
Copy link
Member

Choose a reason for hiding this comment

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

요렇게 해당 부분에서 타입을 지정해도 되지만, useQueryuseMutation사용한 쿼리 함수 작성 부분에서 미리 타입을 정의해주면 이 부분에서 쓰지 않아도 돼요!! 예를 들어

쿼리 결과 값을 타입 지정할 수 있게 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),
  });
};

리팩토링 할 때 더 공부해서 한번에 해도 될 것 같아요 👍 참고 정도로!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

오오 감사합니다!! usemutaion, useQuery를 사용한다면 이 쿼리함수 기능 등을 더 잘 공부해서 적용해봐야겠어요.!

}
);
};

const resetPopup = () => {
setCategoryTitle("");
setIsPopError(false);
setErrorTxt("");
};
Comment on lines +43 to +47
Copy link
Member

Choose a reason for hiding this comment

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

함수 분리 굿굿 👍


return {
options,
categoryTitle,
setCategoryTitle,
isPopError,
errorTxt,
saveCategory,
resetPopup,
};
};
Loading
Loading