Conversation
서버에서 Webhook CRUD가 OpenAPI(API Key)에서 웹 로그인(JWT) 기반으로 이동(datagsm-server#353)하여, API Key로 호출하는 공개 Webhook 관리 API가 더 이상 존재하지 않으므로 관련 문서를 모두 제거합니다. - /api/webhook 문서 7개 페이지 삭제 (개요·등록·목록·수정·삭제·이벤트 명세·서명 검증) - 네비게이션(constants.ts)에서 Webhook 블록 제거 - /api/http 엔드포인트 표의 Webhook 행 및 안내 링크 제거 - /api 권한 범위 표의 WEBHOOK_WRITE 행 제거
Webhook CRUD가 웹 로그인(JWT) 기반으로 전환됨에 따라(datagsm-server#353), client 앱에 로그인 사용자가 본인 Webhook을 등록·조회·수정·삭제하는 UI를 추가합니다. 인증은 기존 axios 인터셉터의 JWT 자동 부착을 그대로 재사용하며, 기존 OAuth Client 관리 기능과 동일한 FSD 구조를 따릅니다. - shared: webhookUrl / webhookQueryKeys 추가 (/v1/webhooks) - shared: 네비게이션에 Webhook 탭 추가 (/webhooks) - entities/webhooks: 타입(서버 snake_case 응답), 이벤트 9종 상수, zod 폼 스키마 - views/webhooks: useGetWebhooks, WebhooksPage (10개 제한 시 추가 버튼 비활성) - widgets/webhooks: 생성/수정/삭제 mutation 훅, 이벤트 선택 훅, 폼 다이얼로그, 목록, secret 1회 노출 성공 다이얼로그
isInitialized ref로 다이얼로그가 열리는 시점에만 폼을 reset하도록 가드를 추가. React Query 자동 refetch로 webhook 참조가 바뀌어도 수정 중인 입력 값이 유지됩니다.
onSubmit이 edit 모드도 처리하도록 통합하고 저장 버튼을 type="submit"으로 변경. 입력 필드에서 엔터를 눌러도 수정 확인 다이얼로그가 정상적으로 열립니다. 중복되던 onSaveClick 핸들러를 제거했습니다.
[feat] 웹에서 Webhook 관리 기능 추가 및 OpenAPI 문서 제거
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Code Review
This pull request introduces a Webhook management interface in the client application, including pages, forms, lists, dialogs, and React Query hooks, while removing the corresponding public API documentation. The review feedback highlights several key improvements: optimizing the useWebhookEventSelection hook by hoisting the watch call, safely accessing the webhook list with optional chaining to prevent runtime errors, adding suppressHydrationWarning to avoid SSR hydration mismatches on date rendering, and replacing hardcoded query keys with centralized query key helpers for better maintainability.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| export const useWebhookEventSelection = ({ watch, setValue }: UseWebhookEventSelectionParams) => { | ||
| const fieldName = 'events'; | ||
|
|
||
| const handleEventToggle = (event: string) => { | ||
| const currentEvents = watch(fieldName) || []; | ||
|
|
||
| setValue( | ||
| fieldName, | ||
| currentEvents.includes(event) | ||
| ? currentEvents.filter((id) => id !== event) | ||
| : [...currentEvents, event], | ||
| { shouldValidate: true, shouldDirty: true }, | ||
| ); | ||
| }; | ||
|
|
||
| const isEventChecked = (event: string) => { | ||
| const currentEvents = watch(fieldName) || []; | ||
|
|
||
| return currentEvents.includes(event); | ||
| }; | ||
|
|
||
| return { | ||
| handleEventToggle, | ||
| isEventChecked, | ||
| }; | ||
| }; |
There was a problem hiding this comment.
useWebhookEventSelection 훅 내부에서 watch('events')가 handleEventToggle과 isEventChecked가 호출될 때마다 반복적으로 실행되고 있습니다. 특히 isEventChecked는 렌더링 중에 모든 이벤트 옵션(현재 9개)에 대해 각각 호출되므로, 매번 watch를 호출하는 것은 비효율적입니다.
watch('events')를 훅의 최상위 레벨에서 한 번만 호출하여 변수에 할당하고, 이를 재사용하도록 개선하면 성능과 가독성을 모두 높일 수 있습니다.
export const useWebhookEventSelection = ({ watch, setValue }: UseWebhookEventSelectionParams) => {
const fieldName = 'events';
const currentEvents = watch(fieldName) || [];
const handleEventToggle = (event: string) => {
setValue(
fieldName,
currentEvents.includes(event)
? currentEvents.filter((id) => id !== event)
: [...currentEvents, event],
{ shouldValidate: true, shouldDirty: true },
);
};
const isEventChecked = (event: string) => {
return currentEvents.includes(event);
};
return {
handleEventToggle,
isEventChecked,
};
};| setIsSuccessDialogOpen(true); | ||
| }; | ||
|
|
||
| const webhooks = webhooksData?.data.webhooks || []; |
There was a problem hiding this comment.
| <TableCell className={cn('text-muted-foreground font-mono text-xs')}> | ||
| {formatDate(webhook.created_at)} | ||
| </TableCell> |
There was a problem hiding this comment.
Next.js의 SSR(Server-Side Rendering) 환경에서 new Date().toLocaleString()을 사용하면 서버와 클라이언트의 시간대(Timezone) 차이로 인해 Hydration Mismatch 경고가 발생할 수 있습니다. 이를 방지하기 위해 해당 TableCell에 suppressHydrationWarning 속성을 추가하는 것을 권장합니다.
| <TableCell className={cn('text-muted-foreground font-mono text-xs')}> | |
| {formatDate(webhook.created_at)} | |
| </TableCell> | |
| <TableCell className={cn('text-muted-foreground font-mono text-xs')} suppressHydrationWarning> | |
| {formatDate(webhook.created_at)} | |
| </TableCell> |
| onSuccess: (data) => { | ||
| setOpen(false); | ||
| queryClient.invalidateQueries({ queryKey: ['webhooks'] }); | ||
| toast.success('웹훅이 생성되었습니다.'); | ||
| onCreateSuccess?.(data.data); | ||
| }, |
There was a problem hiding this comment.
쿼리 키를 무효화(invalidate)할 때 하드코딩된 문자열 배열 ['webhooks'] 대신, 공통으로 정의된 webhookQueryKeys.getWebhooks()를 사용하는 것이 안전하고 유지보수에 좋습니다. 이렇게 하면 추후 쿼리 키 구조가 변경되더라도 일관성을 유지할 수 있습니다.
참고: @repo/shared/api에서 webhookQueryKeys를 임포트하여 사용해야 합니다.
| onSuccess: (data) => { | |
| setOpen(false); | |
| queryClient.invalidateQueries({ queryKey: ['webhooks'] }); | |
| toast.success('웹훅이 생성되었습니다.'); | |
| onCreateSuccess?.(data.data); | |
| }, | |
| onSuccess: (data) => { | |
| setOpen(false); | |
| queryClient.invalidateQueries({ queryKey: webhookQueryKeys.getWebhooks() }); | |
| toast.success('웹훅이 생성되었습니다.'); | |
| onCreateSuccess?.(data.data); | |
| }, |
| onSuccess: () => { | ||
| setOpen(false); | ||
| queryClient.invalidateQueries({ queryKey: ['webhooks'] }); | ||
| toast.success('웹훅이 수정되었습니다.'); | ||
| }, |
There was a problem hiding this comment.
쿼리 키를 무효화할 때 하드코딩된 ['webhooks'] 대신 공통 정의된 webhookQueryKeys.getWebhooks()를 사용하여 일관성을 유지하는 것을 권장합니다.
참고: @repo/shared/api에서 webhookQueryKeys를 임포트하여 사용해야 합니다.
| onSuccess: () => { | |
| setOpen(false); | |
| queryClient.invalidateQueries({ queryKey: ['webhooks'] }); | |
| toast.success('웹훅이 수정되었습니다.'); | |
| }, | |
| onSuccess: () => { | |
| setOpen(false); | |
| queryClient.invalidateQueries({ queryKey: webhookQueryKeys.getWebhooks() }); | |
| toast.success('웹훅이 수정되었습니다.'); | |
| }, |
| onSuccess: () => { | ||
| queryClient.invalidateQueries({ queryKey: ['webhooks'] }); | ||
| toast.success('웹훅이 삭제되었습니다.'); | ||
| }, |
There was a problem hiding this comment.
쿼리 키를 무효화할 때 하드코딩된 ['webhooks'] 대신 공통 정의된 webhookQueryKeys.getWebhooks()를 사용하여 일관성을 유지하는 것을 권장합니다.
참고: @repo/shared/api에서 webhookQueryKeys를 임포트하여 사용해야 합니다.
| onSuccess: () => { | |
| queryClient.invalidateQueries({ queryKey: ['webhooks'] }); | |
| toast.success('웹훅이 삭제되었습니다.'); | |
| }, | |
| onSuccess: () => { | |
| queryClient.invalidateQueries({ queryKey: webhookQueryKeys.getWebhooks() }); | |
| toast.success('웹훅이 삭제되었습니다.'); | |
| }, |
개요 💡
#169 ~ #178 의 내용을 상용환경에 적용합니다.