Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3632099
feat: 익스텐션 화면 초기 세팅
jllee000 Sep 7, 2025
acf449c
feat: 익스텐션 팝업 레이아웃
jllee000 Sep 7, 2025
1593a89
Merge remote-tracking branch 'origin/develop' into feat/#62/extension…
jllee000 Sep 7, 2025
93483a9
feat: 컴포넌트 조합 시작
jllee000 Sep 7, 2025
5976c0c
Merge remote-tracking branch 'origin/develop' into feat/#62/extension…
jllee000 Sep 8, 2025
df1d6b7
chore: 빌드에러 사항 수정
jllee000 Sep 9, 2025
c9a12fb
feat: 익스텐션 화면 레이아웃
jllee000 Sep 9, 2025
da85f3f
feat: 데이터 fetch 구간
jllee000 Sep 9, 2025
9203a31
feat: 도메인 썸네일 및 정보 불러오기
jllee000 Sep 9, 2025
2966daa
feat: 북마크 저장 로직
jllee000 Sep 9, 2025
dc223a2
feat: 리마인드 시간 유효성 검사
jllee000 Sep 9, 2025
0f09c02
feat: 팝업 화면 연동로직
jllee000 Sep 9, 2025
34cacb1
Merge remote-tracking branch 'origin/develop' into feat/#62/extension…
jllee000 Sep 9, 2025
c9efabb
feat: 드롭다운 연결 및 리팩토링
jllee000 Sep 9, 2025
c8a0041
chore: 빌드에러 수정
jllee000 Sep 9, 2025
f84798d
chore: 주석제거
jllee000 Sep 9, 2025
8f3576c
chore: 주석 제거
jllee000 Sep 10, 2025
6240c9b
feat: 코드리뷰 반영
jllee000 Sep 10, 2025
991d984
feat: 아이콘 빌드
jllee000 Sep 10, 2025
87dcb1c
feat: 아이콘 svg 연결
jllee000 Sep 10, 2025
7349edf
feat: 유효성 검사 공통 유틸 ds 이동
jllee000 Sep 10, 2025
31c1382
fix: 가독성 측면 줄바꿈
jllee000 Sep 10, 2025
238bafa
feat: 팝업 오픈 핸들링 수정
jllee000 Sep 10, 2025
2361d30
feat: 카테고리 추가 에러 prop 추가
jllee000 Sep 10, 2025
08a4df3
feat: 카테고리 추가 에러 연동
jllee000 Sep 10, 2025
8297730
chore: 콘솔 제거
jllee000 Sep 10, 2025
9937c01
chore: 머지 충돌 에러 수정
jllee000 Sep 10, 2025
889707f
feat: 코드리뷰 수정 반영
jllee000 Sep 10, 2025
1a5919b
feat: 스토리북 수정
jllee000 Sep 10, 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
7 changes: 4 additions & 3 deletions apps/extension/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@
"version": "0.1.0",
"action": { "default_popup": "popup.html" },
"icons": {
"128": "icon128.png"
"128": "icon.png"
},
"permissions": ["activeTab", "tabs", "storage", "scripting", "bookmarks"],
"background": {
"service_worker": "src/background.ts",
"service_worker": "src/background.js",
"type": "module"
},
Comment on lines +11 to 13
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

TS 엔트리 → .js로 변경은 빌드 파이프라인과 충돌 위험(소스에 .js 미존재)

레포는 TS 소스(src/background.ts, src/content.ts)를 사용 중인데 매니페스트가 .js를 직접 가리키면 번들러(예: vite-plugin-web-extension)가 엔트리 추적을 못 해 빌드/핫리로드가 깨질 수 있습니다. 실제로 소스 트리에 src/background.js/src/content.js는 없을 확률이 큽니다. TS 엔트리를 그대로 두고 번들러가 JS를 산출하도록 되돌리는 것을 권장합니다.

적용 제안:

   "background": {
-    "service_worker": "src/background.js",
+    "service_worker": "src/background.ts",
     "type": "module"
   },
@@
       "matches": ["<all_urls>"],
-      "js": ["src/content.js"]
+      "js": ["src/content.ts"]

검증 스크립트(소스에 .js가 없는지, TS가 존재하는지 확인):

Also applies to: 17-18


🏁 Script executed:

#!/bin/bash
fd -a 'background.js' apps/extension/src || echo "no background.js in src (expected)"
fd -a 'content.js' apps/extension/src || echo "no content.js in src (expected)"
fd -a 'background.ts' apps/extension/src
fd -a 'content.ts' apps/extension/src

Length of output: 100


manifest.json에서 service_worker·content 경로를 .ts로 수정 필요
실제 apps/extension/srcbackground.js/content.js가 없고 background.ts/content.ts만 존재하여(스크립트로 확인), 번들러가 엔트리를 찾지 못해 빌드·핫리로드가 깨집니다. 아래처럼 .js.ts로 변경하세요.

   "background": {
-    "service_worker": "src/background.js",
+    "service_worker": "src/background.ts",
     "type": "module"
   },
@@
       "matches": ["<all_urls>"],
-      "js": ["src/content.js"]
+      "js": ["src/content.ts"]
🤖 Prompt for AI Agents
In apps/extension/manifest.json around lines 11 to 13, the manifest points
service_worker and content script files to .js files that don't exist; update
the paths to reference the actual TypeScript sources (e.g., change
"src/background.js" → "src/background.ts" and any "content.js" →
"src/content.ts") so the bundler can find the entry points and restore
build/hot-reload.

"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["src/content.ts"]
"js": ["src/content.js"]
}
],
"host_permissions": ["<all_urls>"]
Expand Down
7 changes: 3 additions & 4 deletions apps/extension/popup.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
<!doctype html>
<html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta charset="UTF-8" />
<title>PinBack Extension</title>
</head>
<body>
<div id="root"></div>
<!-- src 경로는 프로젝트 루트 기준으로 모듈 import -->
<script type="module" src="/src/popup.tsx"></script>
</body>
</html>
Binary file added apps/extension/public/icon.png
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.

너가 더.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed apps/extension/public/icon128.png
Binary file not shown.
173 changes: 170 additions & 3 deletions apps/extension/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,177 @@
import './App.css';

import {
InfoBox,
Button,
Textarea,
DateTime,
Switch,
PopupContainer,
Dropdown,
validateDate,
validateTime
} from '@pinback/design-system/ui';
import { useState } from 'react';
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 handleDateChange = (value: string) => {
setDate(value);
setDateError(validateDate(value));
};

const handleTimeChange = (value: string) => {
setTime(value);
setTimeError(validateTime(value));
};

// 스위치
const handleSwitchChange = (checked: boolean) => {
setIsRemindOn(checked);
};

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);
};
Comment on lines +46 to +72
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

저장 전 유효성 게이트 + 불필요한 로그/중복 객체 제거 + await 누락

  • 리마인드 ON인데 에러가 있는 경우 저장을 막아야 합니다.
  • 동일 payload를 다시 만들어 로그만 찍는 코드는 제거하세요(ESLint: Unexpected console).
  • save는 Promise이므로 await로 완료 보장(훅 내부에서 window.close 호출).
-  const handleSave = async () => {
-    save({
+  const handleSave = async () => {
+    if (isRemindOn) {
+      const dErr = validateDate(date);
+      const tErr = validateTime(time);
+      setDateError(dErr);
+      setTimeError(tErr);
+      if (dErr || tErr) return;
+    }
+    await 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);
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const 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 handleSave = async () => {
if (isRemindOn) {
const dErr = validateDate(date);
const tErr = validateTime(time);
setDateError(dErr);
setTimeError(tErr);
if (dErr || tErr) return;
}
await save({
url,
title,
description,
imgUrl,
memo,
isRemindOn,
selectedCategory: selected,
date: isRemindOn ? date : null,
time: isRemindOn ? time : null,
});
};
🧰 Tools
🪛 GitHub Check: lint

[warning] 70-70:
Unexpected console statement

🤖 Prompt for AI Agents
In apps/extension/src/App.tsx around lines 46 to 71, add a pre-save validation
that prevents calling save when isRemindOn is true and there are validation
errors (e.g., missing/invalid date or time), remove the duplicate saveData
object and the console.log that only re-creates the payload, and call await
save(...) so the Promise completes before proceeding (so any subsequent
window.close or follow-up logic in the hook runs after save finishes); keep the
payload construction as originally passed to save and ensure date/time are set
to null when isRemindOn is false.

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="flex h-[50rem] w-[26rem] items-center justify-center bg-blue-500 text-2xl text-white">
자 핀백 앱잼 시작~오늘은 7월 7일임~
<div className="relative flex h-[56.8rem] w-[31.2rem] items-center justify-center">
{isPopupOpen && (
<PopupContainer
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.

input에서 입력값 받는 부분, 하단에 에러메시지 띄우는 부분이 공컴 작업에서 누락이 되었었나봐요!

해당 기능 추가해두었습니다~!

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>
Comment on lines +147 to +164
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

DateTime(time) 자식 → onChange 미전파로 handleTimeChange가 호출되지 않을 가능성

디자인시스템 DateTime.tsx를 보면 time 타입에서 props.onChange를 호출하지 않습니다(현재는 date 타입만 onChange 전달). App에서 onChange={handleTimeChange}가 무의미해져 time 상태/에러가 갱신되지 않을 수 있습니다.

검증 스크립트(자식 컴포넌트의 onChange 호출 여부):

빠른 수정 제안(DateTime.tsx 내부, time 분기에서 부모 onChange 호출):

// beforeInput insert/삭제/붙여넣기 각 분기에서 setInput(...) 뒤에 추가
-      setInput(formatTime12(next));
+      const formatted = formatTime12(next);
+      setInput(formatted);
+      props.onChange?.(formatted);
...
-      setInput(formatTime12(next));
+      const formatted = formatTime12(next);
+      setInput(formatted);
+      props.onChange?.(formatted);
...
-      setInput(formatTime12(next));
+      const formatted = formatTime12(next);
+      setInput(formatted);
+      props.onChange?.(formatted);

🏁 Script executed:

#!/bin/bash
rg -n 'DateTime\.tsx' packages/design-system/src/components/dateTime -n
rg -nP 'onChange\(.*\)' packages/design-system/src/components/dateTime/DateTime.tsx -n
rg -nP 'type === .time.*\)' -n -C3 packages/design-system/src/components/dateTime/DateTime.tsx

Length of output: 0


🏁 Script executed:

#!/bin/bash
# DateTime 컴포넌트 파일 경로 찾기
FILE=$(find packages/design-system -type f -iname "DateTime.tsx" | head -n1)
if [ -z "$FILE" ]; then
  echo "DateTime.tsx 파일을 찾을 수 없습니다."
  exit 0
fi

echo "Inspecting: $FILE"

echo -e "\n--- props.onChange 호출 여부 ---"
rg -n 'props\.onChange' "$FILE" || echo "없음"

echo -e "\n--- time 타입 분기 코드 ---"
rg -nP "type\\s*[:=]\\s*['\"]time" -nC3 "$FILE" || echo "없음"

Length of output: 587


🏁 Script executed:

#!/bin/bash
# DateTime.tsx 길이 및 time 분기 확인
FILE=$(find packages/design-system -type f -iname "DateTime.tsx" | head -n1)
if [ -z "$FILE" ]; then
  echo "DateTime.tsx 파일을 찾을 수 없습니다."
  exit 1
fi

echo "파일 경로: $FILE"
echo "총 라인 수:"
wc -l "$FILE"

echo -e "\n--- time 타입 분기 주변 코드 (컨텍스트 5줄) ---"
rg -n -C5 "type\\s*===\\s*['\"]time['\"]" "$FILE" || echo "time 분기 없음"

echo -e "\n--- props.onChange 호출 위치 (컨텍스트 5줄) ---"
rg -n -C5 "props\\.onChange" "$FILE" || echo "props.onChange 호출 없음"

Length of output: 790


🏁 Script executed:

#!/bin/bash
# DateTime.tsx에서 formatTime 사용 및 onChange 호출 확인
FILE=$(find packages/design-system -type f -iname "DateTime.tsx" | head -n1)
if [ -z "$FILE" ]; then
  echo "DateTime.tsx 파일을 찾을 수 없습니다."
  exit 1
fi

echo "파일 경로: $FILE"
echo

echo "--- formatTime 사용 위치 (컨텍스트 3줄) ---"
rg -n -C3 "formatTime" "$FILE" || echo "formatTime 없음"

echo
echo "--- formatTime 분기에서 props.onChange 호출 여부 (컨텍스트 3줄) ---"
rg -n -C3 "formatTime.*props\.onChange" "$FILE" || echo "formatTime 이후 onChange 호출 없음"

Length of output: 1646


DateTime(time) onChange 미호출로 handleTimeChange 미실행
packages/extension/src/App.tsx(131–148)에서 <DateTime type="time" onChange={handleTimeChange} /> 사용 시, 디자인시스템의 time 분기(90–144라인) 로직에 props.onChange 호출이 없어 상위 handleTimeChange가 실행되지 않습니다. 아래처럼 setInput 직후에 호출을 추가해야 합니다.

@@ packages/design-system/src/components/dateTime/DateTime.tsx
-      setInput(formatTime12(next));
+      const formatted = formatTime12(next);
+      setInput(formatted);
+      props.onChange?.(formatted);

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

🤖 Prompt for AI Agents
In apps/extension/src/App.tsx around lines 131 to 148, the DateTime component
for type="time" updates internal input state but does not call props.onChange,
so the parent handleTimeChange never runs; after the setInput call in the
time-handling branch of the design system component (around lines 90–144
referenced in the comment), invoke props.onChange with the normalized time value
(or the same value passed into setInput), guarding that props.onChange exists
before calling it, so the parent handler receives the update.


{/* 에러 메시지 출력 */}
{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>
);
Expand Down
3 changes: 3 additions & 0 deletions apps/extension/src/assets/logo.svg
Copy link
Collaborator

Choose a reason for hiding this comment

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

로고가 모든 페이지에서 대부분 사용된다고 생각이드는데 디자인시스템에 저장하고 하나를 재사용하는 방싣에 대해 어떻게 생각하시나요?

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 39 additions & 1 deletion apps/extension/src/background.ts
Original file line number Diff line number Diff line change
@@ -1 +1,39 @@
console.log("백그라운드 기능")
console.log('백그라운드 기능');
Copy link
Member

Choose a reason for hiding this comment

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

불필요한 콘솔 제거!

chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message.type === 'FETCH_OG_META') {
fetch(message.url)
.then((res) => res.text())
.then((html) => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');

Comment on lines +7 to +9
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

MV3 서비스 워커에서 DOMParser 사용 불가 → 오프스크린 문서나 컨텐트 스크립트로 이전 필요.

서비스 워커에는 DOM이 없어 DOMParser를 쓸 수 없습니다. chrome.offscreen(reasons: ["DOM_PARSER"])로 오프스크린 문서를 띄워 파싱하거나, 컨텐트 스크립트에서 현재 탭 DOM으로부터 OG 메타를 읽어오세요. 현 구현은 런타임에서 ReferenceError를 유발할 가능성이 큽니다. (stackoverflow.com, developer.chrome.com)

원하시면 오프스크린 문서 기반으로 리팩터를 제안드리겠습니다.

🤖 Prompt for AI Agents
In apps/extension/src/background.ts around lines 7-9, the code uses DOMParser in
the MV3 service worker (parser.parseFromString(html, 'text/html')), which will
throw ReferenceError; move DOM parsing out of the service worker by either 1)
creating an offscreen document via chrome.offscreen.createDocument({reason:
"DOM_PARSER", url: "offscreen.html"}) and send the HTML to that offscreen page
(via chrome.runtime messaging) to run DOMParser there and return the parsed
OG/meta results, ensuring you create and close the offscreen document only when
needed, or 2) run a content script in the target tab to read OG/meta directly
from the page DOM and return it to the background service worker; implement one
of these flows and replace the direct DOMParser call in the background file with
messaging to the offscreen page or content script.

const getMeta = (prop) =>
doc
.querySelector(`meta[property="${prop}"]`)
?.getAttribute('content') || '';

const makeAbsoluteUrl = (base, img) => {
try {
return img ? new URL(img, base).href : '';
} catch {
return img;
}
};

const image = getMeta('og:image');

sendResponse({
title: getMeta('og:title'),
description: getMeta('og:description'),
siteName: getMeta('og:site_name'),
image: makeAbsoluteUrl(message.url, image),
url: getMeta('og:url') || message.url,
});
Comment on lines +4 to +31
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

페치 안정성: 타임아웃·스킴 검증 추가, 호스트 권한 점검 필요.

  • 무한 대기 방지를 위해 AbortController로 타임아웃을 넣으세요.
  • http/https 외 스킴은 즉시 거부하세요.
  • 크로스 오리진 HTML을 가져오려면 host_permissions가 필요합니다. 미설정 시 다수 사이트에서 CORS로 실패합니다. (developer.chrome.com)
   if (message.type === 'FETCH_OG_META') {
-    fetch(message.url)
+    try {
+      if (!/^https?:\/\//i.test(message.url)) throw new Error('unsupported scheme');
+    } catch {
+      sendResponse(null);
+      return true;
+    }
+    const ac = new AbortController();
+    const to = setTimeout(() => ac.abort(), 8000);
+    fetch(message.url, { signal: ac.signal })
       .then((res) => res.text())
       .then((html) => {
+        clearTimeout(to);
         const parser = new DOMParser();

권한 확인 스크립트:


🏁 Script executed:

#!/bin/bash
# manifest.json에서 권한 확인
rg -n --json '"(permissions|host_permissions)"' apps/extension/manifest.json | jq -r '.data.lines.text' || true

Length of output: 130


Fetch 안정성 강화 및 스킴 검증

  • AbortController로 타임아웃(예: 8000ms) 처리해 무한 대기 방지
  • if (!/^https?:\/\//i) 검사를 통해 http/https 외 스킴 즉시 거부
  • manifest.json에 이미 "host_permissions": ["<all_urls>"] 가 설정돼 있어 크로스 오리진 fetch 권한 확보됨

})
.catch((err) => {
console.error('OG fetch 실패:', err);
sendResponse(null);
});
return true; // async 응답
}
});
2 changes: 1 addition & 1 deletion apps/extension/src/content.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
console.log("컨텐츠 스크립트")
console.log('컨텐츠 스크립트 로드됨');
55 changes: 55 additions & 0 deletions apps/extension/src/hooks/usePageMeta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useEffect, useState } from 'react';
import { OgImageFetcher } from '@utils/OGFetch';

export interface PageMeta {
url: string;
title: string;
description: string;
imgUrl: string;
}
Comment on lines +4 to +9
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

프로퍼티 네이밍 정합성: imgUrl → imageUrl 권장

디자인 시스템/OG 응답과 용어 통일을 위해 imgUrl 대신 imageUrl 사용을 권장합니다. 전체 사용처 영향이 있으므로 리팩터 시 레퍼런스 업데이트 확인 필요.

 export interface PageMeta {
   url: string;
   title: string;
   description: string;
-  imgUrl: string;
+  imageUrl: string;
 }
@@
   const [meta, setMeta] = useState<PageMeta>({
     url: '',
     title: '',
     description: '',
-    imgUrl: '',
+    imageUrl: '',
   });
@@
-        imgUrl: ogMeta?.image ?? '',
+        imageUrl: ogMeta?.image ?? '',

검증 스크립트:

Also applies to: 16-16, 45-46


🏁 Script executed:

#!/bin/bash
# imgUrl 사용처 점검
rg -nP '\bimgUrl\b' -C2

Length of output: 4946


imgUrl → imageUrl로 프로퍼티 이름 통일 및 일괄 리팩토링 필요
다음 파일들에서 imgUrlimageUrl로 일괄 변경하세요:

  • packages/design-system/src/components/infobox/InfoBox.tsx (InfoboxProps 및 컴포넌트 파라미터)
  • packages/design-system/src/components/infobox/InfoBox.stories.tsx (args)
  • apps/extension/src/hooks/usePageMeta.ts (PageMeta 인터페이스, 초기 state, ogMeta 매핑)
  • apps/extension/src/hooks/useSaveBookmarks.ts (hook 인자 타입)
  • apps/extension/src/App.tsx (usePageMeta destructuring, saveBookmark 호출, 컴포넌트 prop 전달)

모든 사용처를 함께 업데이트하여 타입 에러 및 런타임 오류를 방지하세요.

🤖 Prompt for AI Agents
In apps/extension/src/hooks/usePageMeta.ts around lines 4 to 9 and in the other
listed files, the PageMeta property name imgUrl must be renamed to imageUrl
consistently; update the PageMeta interface, any initial state defaults, and
mapping from ogMeta to use imageUrl instead of imgUrl, and then propagate that
rename across all usages: change InfoboxProps and component parameter names in
packages/design-system/src/components/infobox/InfoBox.tsx, update story args in
packages/design-system/src/components/infobox/InfoBox.stories.tsx, adjust the
hook argument type in apps/extension/src/hooks/useSaveBookmarks.ts, and fix all
call sites in apps/extension/src/App.tsx (destructuring from usePageMeta,
saveBookmark invocation, and props passed to components) so that every reference
uses imageUrl to prevent type and runtime errors.


export const usePageMeta = () => {
const [meta, setMeta] = useState<PageMeta>({
url: '',
title: '',
description: '',
imgUrl: '',
});

useEffect(() => {
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
const activeTab = tabs[0];
if (!activeTab?.url) return;

const currentUrl = activeTab.url;

chrome.storage.local.set({ bookmarkedUrl: currentUrl });

const imageUrl = await OgImageFetcher({ url: currentUrl });
// 개발중에는 잠시 주석처리
// const isInternalChromePage =
// /^chrome:\/\//.test(currentUrl) ||
// /^edge:\/\//.test(currentUrl) ||
// /^about:/.test(currentUrl);
// // chrome-extension:// 은 내부 페이지로 취급하지 않음

// if (isInternalChromePage || !imageUrl?.title) {
// window.close();
// return;
// }

const newMeta = {
url: currentUrl,
title: imageUrl.title ?? '',
description: imageUrl.description ?? '',
imgUrl: imageUrl.image ?? '',
};

setMeta(newMeta);

chrome.storage.local.set({ titleSave: newMeta.title });
});
}, []);

return meta;
};
Comment on lines +19 to +57
Copy link
Member

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.

오 피드백 감사합니다! 역할에 따라 분리해두었습니다

52 changes: 52 additions & 0 deletions apps/extension/src/hooks/useSaveBookmarks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
interface SaveBookmarkParams {
url: string;
title: string;
description: string;
imgUrl: string;
memo: string;
isRemindOn: boolean;
selectedCategory: string | null;
date: string | null;
time: string | null;
}

export const useSaveBookmark = () => {
const save = async (params: SaveBookmarkParams) => {
try {
const saveData = {
...params,
createdAt: new Date().toISOString(),
};

console.log('저장 데이터:', saveData);
Copy link
Member

Choose a reason for hiding this comment

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

여기도 테스트용 콘솔 지워주세요!


const result = await new Promise<{ bookmarks?: any[] }>((resolve) => {
chrome.storage.local.get(['bookmarks'], (items) => resolve(items));
});

const bookmarks = result.bookmarks || [];
bookmarks.push(saveData);

await new Promise<void>((resolve) => {
chrome.storage.local.set({ bookmarks }, resolve);
});

chrome.bookmarks.create(
{
parentId: '1',
title: params.title || params.url,
url: params.url,
},
(newBookmark) => {
console.log('크롬 북마크바에 저장 완료:', newBookmark);
}
);
Comment on lines +32 to +41
Copy link

@coderabbitai coderabbitai bot Sep 9, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

북마크바 parentId 하드코딩(‘1’)은 환경에 따라 깨집니다. 동적으로 해석하고 Promise 기반 API로 대체하세요.

폴더 ID는 고정이 아닙니다. folderType: "bookmarks-bar"로 탐색해 parentId를 찾은 뒤 생성하세요. 콜백 대신 Promise를 사용해 window.close() 시점도 보장하면 좋습니다. (developer.chrome.com, groups.google.com)

-      chrome.bookmarks.create(
-        {
-          parentId: '1',
-          title: params.title || params.url,
-          url: params.url,
-        },
-        (newBookmark) => {
-          console.log('크롬 북마크바에 저장 완료:', newBookmark);
-        }
-      );
+      // 북마크바 ID 해석
+      const tree = await chrome.bookmarks.getTree();
+      const findBar = (nodes: chrome.bookmarks.BookmarkTreeNode[]): string | null => {
+        for (const n of nodes) {
+          // Chrome 134+ folderType 제공. 없으면 이름/루트 자식으로 폴백.
+          if ((n as any).folderType === 'bookmarks-bar') return n.id;
+          if (n.children?.length) {
+            const r = findBar(n.children);
+            if (r) return r;
+          }
+        }
+        return null;
+      };
+      const parentId = findBar(tree) ?? undefined; // undefined면 "기타 북마크"로 생성
+
+      const newBookmark = await chrome.bookmarks.create({
+        parentId,
+        title: params.title || params.url,
+        url: params.url,
+      });
+      if (process.env.NODE_ENV !== 'production') {
+        console.log('크롬 북마크 저장 완료:', newBookmark);
+      }

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

🧰 Tools
🪛 GitHub Check: lint

[warning] 41-41:
Unexpected console statement

🤖 Prompt for AI Agents
In apps/extension/src/hooks/useSaveBookmarks.ts around lines 34-43, the code
hardcodes parentId '1' and uses the callback API; change it to dynamically
locate the bookmarks bar folder (search the bookmarks tree for node with
folderType "bookmarks-bar" or equivalent titles/ids) to obtain the correct
parentId, then wrap chrome.bookmarks.create in a Promise (or use the
promise-based chrome.bookmarks API) instead of a callback, await the creation,
and only call window.close() after the Promise resolves; handle the case where
the bookmarks-bar node is not found by falling back to creating at the root or
creating the bookmarks-bar folder first and surface/log errors appropriately.

Copy link
Member

Choose a reason for hiding this comment

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

요 부분은 확인 안해도 괜찮을까요??

Copy link

Choose a reason for hiding this comment

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

@constantly-dev 네, 현재 단계에서는 넘어가셔도 괜찮습니다!

하드코딩된 parentId: '1'즉시 수정해야 할 크리티컬한 이슈는 아니지만, 다음과 같은 잠재적 위험이 있습니다:

  • Chrome 환경에 따라 북마크바 ID가 다를 수 있음
  • 일부 사용자에게서 북마크 저장이 실패할 가능성

권장사항:

  1. 현재: 기본 기능 구현에 집중하시고 이 부분은 추후 개선
  2. 나중에: 별도 이슈로 등록해서 북마크바 동적 탐지 로직 추가

현재 PR의 주요 목표인 익스텐션 레이아웃과 북마크 저장 기본 기능이 우선이므로, 이 부분은 기술부채로 남겨두고 나중에 개선하셔도 충분합니다.

진행하시면 됩니다! 👍


🧠 Learnings used
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#2
File: pnpm-workspace.yaml:3-3
Timestamp: 2025-08-18T13:48:59.065Z
Learning: constantly-dev는 docs 디렉터리를 컨벤션 문서 추가용으로 사용할 예정이라고 명시했습니다.


// window.close();
} catch (error) {
console.error('저장 중 오류:', error);
}
};

return { save };
};
4 changes: 1 addition & 3 deletions apps/extension/src/popup.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// src/popup.tsx
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
Expand All @@ -14,4 +12,4 @@ if (rootEl) {
);
} else {
console.error('❌ root element not found!');
}
}
Loading