Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
a9e35bd
Feat(client): Popup 컴포넌트에 체크박스 옵션 추가 및 PopupPortal에서 공유 기능 구현
jjangminii Feb 24, 2026
1b8ced5
Merge branch 'develop' of https://github.com/Pinback-Team/pinback-cli…
jjangminii Feb 24, 2026
e7f1541
Feat(client): Checkbox 컴포넌트에 xsmall 사이즈 추가
jjangminii Feb 24, 2026
f5ce499
Feat(client): 카테고리 생성 API에 공개 여부 추가
jjangminii Feb 25, 2026
e8c5489
Merge branch 'develop' of https://github.com/Pinback-Team/pinback-cli…
jjangminii Feb 25, 2026
3bf8c23
Feat(client): 카테고리 수정 API PATCH 메서드로 변경 및 관련 훅 업데이트
jjangminii Feb 25, 2026
41db846
Feat(client): 카테고리 관련 API 및 컴포넌트 수정 - 공개 여부 추가 및 관련 로직 업데이트
jjangminii Feb 26, 2026
218095a
fix: 빌드 오류 해결
jjangminii Feb 26, 2026
4d4eb03
Feat: 익스텐션 카테고리 API 및 컴포넌트 수정
jjangminii Feb 26, 2026
b389a49
Merge branch 'develop' of https://github.com/Pinback-Team/pinback-cli…
jjangminii Feb 26, 2026
3b5607a
Merge branch 'develop' of https://github.com/Pinback-Team/pinback-cli…
jjangminii Feb 26, 2026
894a4ea
feat: 카테고리 상세 정보 조회 기능 추가
jjangminii Feb 26, 2026
947b878
fix: 사용하지 않는 categoryDetail 데이터 제거 및 api.ts에서 isPublic 필드 삭제
jjangminii Feb 27, 2026
4aaa013
feat: 카테고리 편집 기능 개선 및 getCategoryDetail 호출 로직 통합
jjangminii Feb 27, 2026
597a4f0
refactor: OptionsMenuPortal에서 isPublic 필드 제거 및 onEdit 함수 시그니처 수정
jjangminii Feb 27, 2026
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
19 changes: 15 additions & 4 deletions apps/client/src/shared/apis/axios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,22 @@ export const getDashboardCategories = async () => {
return data.data;
};

export const postCategory = async (categoryName: string) => {
const response = await apiRequest.post('/api/v1/categories', {
export const postCategory = async (categoryName: string, isPublic: boolean) => {
const response = await apiRequest.post('/api/v3/categories', {
categoryName,
isPublic,
});
return response;
};

export const putCategory = async (id: number, categoryName: string) => {
const response = await apiRequest.put(`/api/v1/categories/${id}`, {
export const patchCategory = async (
id: number,
categoryName: string,
isPublic: boolean
) => {
const response = await apiRequest.patch(`/api/v3/categories/${id}`, {
categoryName,
isPublic,
});
return response;
};
Expand Down Expand Up @@ -97,3 +103,8 @@ export const patchUserJob = async (requestData: patchUserJobRequest) => {
const { data } = await apiRequest.patch('/api/v3/users/job', requestData);
return data;
};

export const getCategoryDetail = async (categoryId: number) => {
const { data } = await apiRequest.get(`/api/v3/categories/${categoryId}`);
return data.data;
};
35 changes: 30 additions & 5 deletions apps/client/src/shared/apis/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,25 @@ import {
deleteRemindArticle,
getAcorns,
getArticleDetail,
getCategoryDetail,
getDashboardCategories,
getGoogleProfile,
getJobs,
getMyProfile,
patchCategory,
patchUserJob,
patchUserJobRequest,
postCategory,
postSignUp,
postSignUpRequest,
putArticleReadStatus,
putCategory,
putEditArticle,
} from '@shared/apis/axios';
import {
AcornsResponse,
ArticleDetailResponse,
ArticleReadStatusResponse,
CategoryDetailResponse,
DashboardCategoriesResponse,
EditArticleRequest,
JobsResponse,
Expand Down Expand Up @@ -47,13 +49,26 @@ export const useGetDashboardCategories = (): UseQueryResult<

export const usePostCategory = () => {
return useMutation({
mutationFn: (categoryName: string) => postCategory(categoryName),
mutationFn: ({
categoryName,
isPublic,
}: {
categoryName: string;
isPublic: boolean;
}) => postCategory(categoryName, isPublic),
});
};
export const usePutCategory = () => {
export const usePatchCategory = () => {
return useMutation({
mutationFn: ({ id, categoryName }: { id: number; categoryName: string }) =>
putCategory(id, categoryName),
mutationFn: ({
id,
categoryName,
isPublic,
}: {
id: number;
categoryName: string;
isPublic: boolean;
}) => patchCategory(id, categoryName, isPublic),
});
};

Expand Down Expand Up @@ -203,3 +218,13 @@ export const usePatchUserJob = () => {
mutationFn: (data: patchUserJobRequest) => patchUserJob(data),
});
};

export const useGetCategoryDetail = (): UseMutationResult<
CategoryDetailResponse,
AxiosError,
number
> => {
return useMutation({
mutationFn: (categoryId: number) => getCategoryDetail(categoryId),
});
};
42 changes: 34 additions & 8 deletions apps/client/src/shared/components/sidebar/OptionsMenuPortal.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { createPortal } from 'react-dom';
import OptionsMenuButton from '@shared/components/optionsMenuButton/OptionsMenuButton';
import { createPortal } from 'react-dom';

interface OptionsMenuPortalProps {
open: boolean;
style?: React.CSSProperties | null;
containerRef: React.RefObject<HTMLDivElement | null>;
categoryId: number | null;
getCategoryName: (id: number | null) => string;
onEdit: (id: number, name: string) => void;
getCategoryName?: (id: number | null) => string;
getCategory?: (id: number | null) => {
id: number;
name: string;
} | null;
onEdit: (id: number) => void;
onDelete: (id: number, name: string) => void;
onClose: () => void;
}
Expand All @@ -18,24 +22,46 @@ export default function OptionsMenuPortal({
containerRef,
categoryId,
getCategoryName,
getCategory,
onEdit,
onDelete,
onClose,
}: OptionsMenuPortalProps) {
if (!open || !style) return null;

const id = categoryId;
const name = getCategoryName(categoryId);
let id: number | null = categoryId;
let name = '';

if (getCategory) {
const category = getCategory(categoryId);

if (!category) return null;

id = category.id;
name = category.name;
} else if (getCategoryName) {
name = getCategoryName(categoryId);
}
Comment on lines 32 to 44
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

getCategorygetCategoryName 둘 다 없는 경우 처리가 누락되었습니다.

현재 로직에서 getCategorygetCategoryName도 제공되지 않으면 name이 빈 문자열로 남게 됩니다. 이 경우 onEditonDelete에 빈 문자열이 전달됩니다.

🛡️ 두 prop 모두 없는 경우 early return 추가 제안
   if (getCategory) {
     const category = getCategory(categoryId);

     if (!category) return null;

     id = category.id;
     name = category.name;
     isPublic = category.isPublic;
   } else if (getCategoryName) {
     name = getCategoryName(categoryId);
+  } else {
+    return null;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/shared/components/sidebar/OptionsMenuPortal.tsx` around lines
33 - 47, The code currently leaves name as an empty string when neither
getCategory nor getCategoryName is provided, causing onEdit/onDelete to receive
empty values; update the logic in the block that reads
getCategory/getCategoryName (the section that sets id, name, isPublic) to
perform an early return (e.g., return null) if both getCategory and
getCategoryName are undefined so you never render or call onEdit/onDelete with
empty name/id; ensure this check happens before any use of name/id/isPublic and
references the existing identifiers getCategory, getCategoryName, id, name,
isPublic, onEdit and onDelete.


return createPortal(
<div ref={containerRef} style={{ ...style, zIndex: 10000 }}>
<div
ref={containerRef}
style={{
...style,
zIndex: 10000,
}}
>
<OptionsMenuButton
onEdit={() => {
if (id != null) onEdit(id, name);
if (id != null) {
onEdit(id);
}
onClose();
}}
onDelete={() => {
if (id != null) onDelete(id, name);
if (id != null) {
onDelete(id, name);
}
onClose();
}}
/>
Expand Down
39 changes: 35 additions & 4 deletions apps/client/src/shared/components/sidebar/PopupPortal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@ interface Props {
popup: PopupState | null;
onClose: () => void;
onChange?: (value: string) => void;
onCreateConfirm?: () => void;
onEditConfirm?: (id: number, draft?: string) => void;

onCreateConfirm?: (shareToJobUsers: boolean) => void;
onEditConfirm?: (
id: number,
draft?: string,
shareToJobUsers?: boolean
) => void;

onDeleteConfirm?: (id: number) => void;
categoryList?: { id: number; name: string }[];
isToastOpen?: boolean;
Expand All @@ -35,9 +41,14 @@ export default function PopupPortal({
}: Props) {
const [draft, setDraft] = useState('');

const [shareToJobUsers, setShareToJobUsers] = useState(true);

useEffect(() => {
if (!popup) return;

setDraft(popup.kind === 'edit' ? (popup.name ?? '') : '');

setShareToJobUsers(popup.kind === 'edit' ? popup.isPublic : false);
}, [popup]);

if (!popup) return null;
Expand Down Expand Up @@ -78,12 +89,12 @@ export default function PopupPortal({

const handleCreate = () => {
if (blocked) return;
onCreateConfirm?.();
onCreateConfirm?.(shareToJobUsers);
};

const handleEdit = () => {
if (blocked || popup.kind !== 'edit') return;
onEditConfirm?.(popup.id, value);
onEditConfirm?.(popup.id, value, shareToJobUsers);
};

const handleDelete = () => {
Expand All @@ -95,6 +106,8 @@ export default function PopupPortal({
const actionLabel =
action === 'create' ? '추가' : action === 'edit' ? '수정' : '삭제';

const showCheckbox = popup.kind === 'create' || popup.kind === 'edit';

return createPortal(
<div className="fixed inset-0 z-[11000]">
<div className="absolute inset-0 bg-black/60" onClick={onClose} />
Expand All @@ -112,6 +125,15 @@ export default function PopupPortal({
placeholder="카테고리 제목을 입력해주세요"
onLeftClick={onClose}
onRightClick={handleCreate}
checkboxOption={
showCheckbox
? {
label: '같은 관심 직무 사용자들에게 공유하기',
isSelected: shareToJobUsers,
onSelectedChange: setShareToJobUsers,
}
: undefined
}
/>
)}

Expand All @@ -127,6 +149,15 @@ export default function PopupPortal({
onInputChange={handleInputChange}
onLeftClick={onClose}
onRightClick={handleEdit}
checkboxOption={
showCheckbox
? {
label: '같은 관심 직무 사용자들에게 공유하기',
isSelected: shareToJobUsers,
onSelectedChange: setShareToJobUsers,
}
: undefined
}
/>
)}

Expand Down
22 changes: 16 additions & 6 deletions apps/client/src/shared/components/sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export function Sidebar() {
handlePatchCategory,
handleDeleteCategory,
handlePopupClose,
handleEditCategory,
} = useCategoryActions({
close,
setActiveTab,
Expand Down Expand Up @@ -117,8 +118,15 @@ export function Sidebar() {
prevAcornRef.current = acornCount;
}, [acornCount, isAcornPending]);

const getCategoryName = (id: number | null) =>
categories?.categories.find((c) => c.id === id)?.name ?? '';
const getCategory = (id: number | null) => {
const c = categories?.categories.find((c) => c.id === id) ?? null;
if (!c) return null;

return {
id: c.id,
name: c.name,
};
};
Comment on lines 121 to 129
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

isPublic 기본값을 true로 강제하면 의도치 않은 공개 전환 위험이 있습니다.

Line 123, Line 208-210의 ?? true 때문에 값이 비어 있는 순간에도 공개로 전파될 수 있습니다. 최소한 true 강제는 제거하고 안전한 기본값(예: false)으로 두는 편이 좋습니다.

🔒 제안 수정안
   const getCategory = (id: number | null) => {
-    const c = categories?.categories.find((c) => c.id === id) ?? null;
+    const c = categories?.categories.find((category) => category.id === id);
     if (!c) return null;
-    return { id: c.id, name: c.name, isPublic: (c as any).isPublic ?? true };
+    return { id: c.id, name: c.name, isPublic: c.isPublic ?? false };
   };
@@
-            onEdit={(id, name, isPublic) =>
-              openEdit(id, name, isPublic ?? true)
-            }
+            onEdit={(id, name, isPublic) => openEdit(id, name, isPublic)}

Also applies to: 208-210

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/shared/components/sidebar/Sidebar.tsx` around lines 120 -
124, The getCategory helper (and the other occurrence using (c as any).isPublic
?? true) wrongly defaults missing isPublic to true; change these to default to
false instead—locate getCategory and the other mapping that sets isPublic,
remove the "?? true" behavior and replace with an explicit boolean coercion or
conditional that yields false when isPublic is null/undefined (e.g., use (typeof
(c as any).isPublic === 'boolean' ? (c as any).isPublic : false) or !!(c as
any).isPublic) so undefined values do not become public.


return (
<aside className="bg-white-bg sticky top-0 h-screen w-[24rem] border-r border-gray-300">
Expand Down Expand Up @@ -197,13 +205,12 @@ export function Sidebar() {
{canCreateMore && <CreateItem onClick={() => openCreate()} />}
</ul>
</AccordionItem>

<OptionsMenuPortal
open={menu.open}
style={style}
categoryId={menu.categoryId}
getCategoryName={getCategoryName}
onEdit={(id, name) => openEdit(id, name)}
getCategory={getCategory}
onEdit={(id) => handleEditCategory(id, openEdit, closeMenu)}
onDelete={(id, name) => openDelete(id, name)}
onClose={closeMenu}
containerRef={containerRef}
Expand Down Expand Up @@ -265,11 +272,14 @@ export function Sidebar() {

{/* Category Popup */}
<PopupPortal
key={popup?.kind === 'edit' ? popup.id : popup?.kind}
popup={popup}
onClose={handlePopupClose}
onChange={handleCategoryChange}
onCreateConfirm={handleCreateCategory}
onEditConfirm={(id) => handlePatchCategory(id)}
onEditConfirm={(id, name, isPublic) =>
handlePatchCategory(id, name, isPublic)
}
onDeleteConfirm={(id) => handleDeleteCategory(id)}
categoryList={categories?.categories ?? []}
isToastOpen={toastIsOpen}
Expand Down
Loading
Loading