Skip to content

Commit 80100db

Browse files
authored
Api(extension): 익스텐션 api (수정,생성) 연결 및 페이지 구조 수정 (#80)
* api: 익스텐션 저장 api 연동 * feat: 일시 조회 및 포맷팅 연결 * feat: duplicate 기준 분리 * feat: 수정 / 추가 분기 로직 * feat: 실제 북마크 저장 위치 수정 * feat: 오늘 날짜데이터 전달 * feat: 카테고리 얼럿 추가 * feat: 메타데이터 에러 분기 * chore: 주석 제거 * feat: 카테고리 연동 및 데이터 구조 수정 * feat: api 서버 도메인 수정 * feat: 토큰 크롬스토리지 구조로 수정 * feat: 카테고리 추가 바로 반영 및 얼럿 추가 * feat: 서비스명 수정 * feat: 리마인드 타임 onChange 조작 수정 * feat: 실시간 포맷 복구 및 blur일때만 부모한테 전달 * feat: 스토리북 코드 수정 * feat: 스토리북 빌드 에러 * feat: 주석 제거 * feat: 스토리북 기본 인풋 placeholder로 채우기 * feat: 임시 토큰 제거 및 api 파일 구조 수정 * feat: api 쿼리 분기 제거 * feat: TODO 표시 * feat: TODO 표시 * chore: 빌드에러 수정 * feat: 코드리뷰 반영 디폴트 썸네일 이미지
1 parent 613b85b commit 80100db

File tree

22 files changed

+680
-315
lines changed

22 files changed

+680
-315
lines changed

apps/extension/popup.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
5-
<title>PinBack Extension</title>
5+
<title>PinBack</title>
66
</head>
77
<body>
88
<div id="root"></div>

apps/extension/src/App.tsx

Lines changed: 29 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -1,179 +1,43 @@
11
import './App.css';
2-
import {
3-
InfoBox,
4-
Button,
5-
Textarea,
6-
DateTime,
7-
Switch,
8-
PopupContainer,
9-
Dropdown,
10-
validateDate,
11-
validateTime
12-
} from '@pinback/design-system/ui';
13-
import { useState } from 'react';
2+
import DuplicatePop from './pages/DuplicatePop';
3+
import MainPop from './pages/MainPop';
4+
import { useState, useEffect } from 'react';
5+
import { useGetArticleSaved } from '@apis/query/queries';
146
import { usePageMeta } from './hooks/usePageMeta';
15-
import { useSaveBookmark } from './hooks/useSaveBookmarks';
16-
import { Icon } from '@pinback/design-system/icons';
17-
const App = () => {
18-
const [isRemindOn, setIsRemindOn] = useState(false);
19-
const [memo, setMemo] = useState('');
20-
const [isPopupOpen, setIsPopupOpen] = useState(false);
217

22-
const [selected, setSelected] = useState<string | null>(null);
23-
// 시간,날짜 검사 구간!
24-
const [date, setDate] = useState('2025.10.10');
25-
const [time, setTime] = useState('19:00');
26-
const [dateError, setDateError] = useState('');
27-
const [timeError, setTimeError] = useState('');
8+
const App = () => {
9+
const { url } = usePageMeta();
10+
const { data: isSaved } = useGetArticleSaved(url);
2811

29-
const handleDateChange = (value: string) => {
30-
setDate(value);
31-
setDateError(validateDate(value));
32-
};
12+
const [isDuplicatePop, setIsDuplicatePop] = useState(false);
13+
const [mainPopType, setMainPopType] = useState<"add" | "edit">("add");
3314

34-
const handleTimeChange = (value: string) => {
35-
setTime(value);
36-
setTimeError(validateTime(value));
37-
};
15+
useEffect(() => {
16+
if (isSaved?.data) {
17+
setIsDuplicatePop(true);
18+
}
19+
}, [isSaved]);
3820

39-
// 스위치
40-
const handleSwitchChange = (checked: boolean) => {
41-
setIsRemindOn(checked);
21+
const handleDuplicateLeftClick = () => {
22+
setIsDuplicatePop(false);
23+
setMainPopType("edit");
4224
};
4325

44-
const { url, title, description, imgUrl } = usePageMeta();
45-
const { save } = useSaveBookmark();
46-
47-
const handleSave = async () => {
48-
save({
49-
url,
50-
title,
51-
description,
52-
imgUrl,
53-
memo,
54-
isRemindOn,
55-
selectedCategory: selected,
56-
date: isRemindOn ? date : null,
57-
time: isRemindOn ? time : null,
58-
});
59-
const saveData = {
60-
url,
61-
title,
62-
description,
63-
imgUrl,
64-
memo,
65-
isRemindOn,
66-
selectedCategory: selected,
67-
date: isRemindOn ? date : null,
68-
time: isRemindOn ? time : null,
69-
createdAt: new Date().toISOString(),
70-
};
71-
console.log('저장된 데이터:', saveData);
26+
const handleDuplicateRightClick = () => {
27+
window.location.href = "/dashboard";
7228
};
73-
const [categoryTitle, setCategoryTitle] = useState('');
74-
const [isPopError, setIsPopError] = useState(false);
75-
const [errorTxt, setErrorTxt] = useState('');
76-
const saveCategory = () => {
77-
if (categoryTitle.length >20){
78-
setIsPopError(true);
79-
setErrorTxt('20자 이내로 작성해주세요');
80-
} else{
81-
setIsPopupOpen(false);
82-
}
83-
}
84-
8529

8630
return (
87-
<div className="App">
88-
<div className="relative flex h-[56.8rem] w-[31.2rem] items-center justify-center">
89-
{isPopupOpen && (
90-
<PopupContainer
91-
isOpen={isPopupOpen}
92-
onClose={() => setIsPopupOpen(false)}
93-
type="input"
94-
title="카테고리 추가하기"
95-
left="취소"
96-
right="확인"
97-
inputValue={categoryTitle}
98-
isError={isPopError}
99-
errortext={errorTxt}
100-
onInputChange={setCategoryTitle}
101-
placeholder="카테고리 제목을 입력해주세요"
102-
onLeftClick={() => setIsPopupOpen(false)}
103-
onRightClick={saveCategory}
104-
/>
105-
)}
106-
<div className="flex flex-col justify-between gap-[1.6rem] rounded-[12px] bg-white px-[3.2rem] py-[2.4rem] text-black">
107-
<div className="mr-auto">
108-
<Icon name="main_logo" width={72} height={20} />
109-
</div>
110-
111-
<InfoBox
112-
title={title || '제목 없음'}
113-
source={description || '웹페이지'}
114-
imgUrl={imgUrl}
115-
/>
116-
117-
<div>
118-
<p className="caption1-sb mb-[0.4rem]">카테고리</p>
119-
<Dropdown
120-
options={['옵션1', '옵션2']}
121-
selectedValue={selected}
122-
onChange={(value) => setSelected(value)}
123-
placeholder="선택해주세요"
124-
onAddItem={() => setIsPopupOpen(true)}
125-
addItemLabel="추가하기"
126-
/>
127-
</div>
128-
129-
<div>
130-
<p className="caption1-sb mb-[0.4rem]">메모</p>
131-
<Textarea
132-
maxLength={100}
133-
placeholder="나중에 내가 꺼내줄 수 있게 살짝 적어줘!"
134-
onChange={(e) => setMemo(e.target.value)}
135-
/>
136-
</div>
137-
138-
<div>
139-
<div className="mb-[0.4rem] flex items-center justify-between">
140-
<p className="caption1-sb">리마인드</p>
141-
<Switch
142-
onCheckedChange={handleSwitchChange}
143-
checked={isRemindOn}
144-
/>
145-
</div>
146-
147-
<div className="mb-[0.4rem] flex items-center justify-between gap-[0.8rem]">
148-
<DateTime
149-
type="date"
150-
state={
151-
dateError ? 'error' : isRemindOn ? 'default' : 'disabled'
152-
}
153-
value={date}
154-
onChange={handleDateChange}
155-
/>
156-
<DateTime
157-
type="time"
158-
state={
159-
timeError ? 'error' : isRemindOn ? 'default' : 'disabled'
160-
}
161-
value={time}
162-
onChange={handleTimeChange}
163-
/>
164-
</div>
165-
166-
{/* 에러 메시지 출력 */}
167-
{dateError && <p className="body3-r text-error">{dateError}</p>}
168-
{timeError && <p className="body3-r text-error">{timeError}</p>}
169-
</div>
170-
171-
<Button size="medium" onClick={handleSave}>
172-
저장
173-
</Button>
174-
</div>
175-
</div>
176-
</div>
31+
<>
32+
{isDuplicatePop ? (
33+
<DuplicatePop
34+
onLeftClick={handleDuplicateLeftClick}
35+
onRightClick={handleDuplicateRightClick}
36+
/>
37+
) : (
38+
<MainPop type={mainPopType} savedData={isSaved?.data}/>
39+
)}
40+
</>
17741
);
17842
};
17943

apps/extension/src/apis/axios.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import apiRequest from "./axiosInstance";
2+
export interface PostArticleRequest {
3+
url: string;
4+
categoryId: number;
5+
memo?: string | null;
6+
remindTime?: string | null;
7+
}
8+
9+
export const postArticle = async (data: PostArticleRequest) => {
10+
const response = await apiRequest.post("/api/v1/articles", data);
11+
return response.data;
12+
};
13+
14+
15+
export interface postSignupRequest {
16+
email: string;
17+
remindDefault: string
18+
fcmToken: string;
19+
}
20+
21+
export const postSignup = async (data: postSignupRequest) => {
22+
const response = await apiRequest.post("/api/v1/auth/signup", data);
23+
return response.data;
24+
};
25+
26+
export const getCategoriesExtension = async () => {
27+
const response = await apiRequest.get("/api/v1/categories/extension");
28+
return response.data;
29+
};
30+
31+
export interface postCategoriesRequest {
32+
categoryName: string;
33+
}
34+
35+
export const postCategories = async (data: postCategoriesRequest) => {
36+
const response = await apiRequest.post("/api/v1/categories", data);
37+
return response.data;
38+
}
39+
40+
export const getRemindTime = async () => {
41+
const now = new Date().toISOString().split(".")[0];
42+
43+
const response = await apiRequest.get("/api/v1/users/remind-time", {
44+
params: { now },
45+
});
46+
47+
return response.data;
48+
};
49+
50+
51+
export const getArticleSaved=async (url:string) => {
52+
const response = await apiRequest.get("/api/v1/articles/saved", {
53+
params: { url },
54+
});
55+
return response.data;
56+
}
57+
58+
export interface PutArticleRequest {
59+
categoryId: number;
60+
memo: string;
61+
now: string;
62+
remindTime: string | null;
63+
}
64+
65+
export const putArticle = async (articleId: number, data: PutArticleRequest) => {
66+
const response = await apiRequest.put(`/api/v1/articles/${articleId}`, data);
67+
return response.data;
68+
};
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { useMutation,useQuery } from "@tanstack/react-query";
2+
import { postArticle, PostArticleRequest,postSignup, postSignupRequest, getCategoriesExtension, postCategories, postCategoriesRequest, getRemindTime, getArticleSaved,putArticle, PutArticleRequest} from "@apis/axios";
3+
4+
export const usePostArticle = () => {
5+
return useMutation({
6+
mutationFn: (data: PostArticleRequest) => postArticle(data),
7+
});
8+
};
9+
10+
export const usePostSignup = () => {
11+
return useMutation({
12+
mutationFn: (data: postSignupRequest) => postSignup(data)
13+
});
14+
}
15+
16+
export const usePostCategories = () => {
17+
return useMutation({
18+
mutationFn: (data: postCategoriesRequest) => postCategories(data),
19+
});
20+
}
21+
export const useGetCategoriesExtension = () => {
22+
return useQuery({
23+
queryKey: ["categoriesExtension"],
24+
queryFn: getCategoriesExtension,
25+
});
26+
};
27+
28+
export const useGetRemindTime = () => {
29+
return useQuery({
30+
queryKey: ["remindTime"],
31+
queryFn: getRemindTime,
32+
});
33+
}
34+
35+
export const useGetArticleSaved = (url:string) => {
36+
return useQuery({
37+
queryKey: ["articleSaved", url],
38+
queryFn: () => getArticleSaved(url),
39+
enabled: !!url,
40+
});
41+
}
42+
43+
export const usePutArticle = () => {
44+
return useMutation({
45+
mutationFn: ({ articleId, data }: { articleId: number; data: PutArticleRequest }) =>
46+
putArticle(articleId, data)
47+
});
48+
};
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { useState } from "react";
2+
import { usePostCategories, useGetCategoriesExtension } from "@apis/query/queries";
3+
import type { Category } from "@shared-types/types";
4+
import { AxiosError } from "axios";
5+
6+
export const useCategoryManager = () => {
7+
const { data: categoryData } = useGetCategoriesExtension();
8+
const { mutate: postCategories } = usePostCategories();
9+
10+
const [categoryTitle, setCategoryTitle] = useState("");
11+
const [isPopError, setIsPopError] = useState(false);
12+
const [errorTxt, setErrorTxt] = useState("");
13+
14+
const options =
15+
categoryData?.data?.categories?.map((c: Category) => c.categoryName) ?? [];
16+
17+
const saveCategory = (onSuccess?: (category: Category) => void) => {
18+
if (categoryTitle.length > 20) {
19+
setIsPopError(true);
20+
setErrorTxt("20자 이내로 작성해주세요");
21+
return;
22+
}
23+
24+
postCategories(
25+
{ categoryName: categoryTitle },
26+
{
27+
onSuccess: (res) => {
28+
const newCategory: Category = {
29+
categoryId: res.data.categoryId,
30+
categoryName: categoryTitle,
31+
categoryColor: res.data.categoryColor ?? "#000000",
32+
};
33+
onSuccess?.(newCategory);
34+
resetPopup();
35+
},
36+
onError: (err: AxiosError<{ code: string; message: string }>) => {
37+
alert(err.response?.data?.message ?? "카테고리 추가 중 오류가 발생했어요 😢");
38+
},
39+
}
40+
);
41+
};
42+
43+
const resetPopup = () => {
44+
setCategoryTitle("");
45+
setIsPopError(false);
46+
setErrorTxt("");
47+
};
48+
49+
return {
50+
options,
51+
categoryTitle,
52+
setCategoryTitle,
53+
isPopError,
54+
errorTxt,
55+
saveCategory,
56+
resetPopup,
57+
};
58+
};

0 commit comments

Comments
 (0)