From d3db91172ab3e89d10c4c4b45aa247fbfca0f6fb Mon Sep 17 00:00:00 2001 From: hyejj19 Date: Wed, 18 Feb 2026 22:21:37 +0900 Subject: [PATCH 01/14] feat: connect dashboard stats to APIs and add navigation - Wire tasting tag, banner, curation counts to real API endpoints - Add curation stat card (was missing entirely) - Filter inquiries to show only WAITING status count - Make all stat cards clickable to navigate to their list pages Co-Authored-By: Claude Opus 4.6 --- src/pages/Dashboard.tsx | 47 +++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 600f483..8d96d66 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -2,20 +2,30 @@ * 대시보드 페이지 */ -import { Wine, MessageSquare, Tag, Image } from 'lucide-react'; +import { useNavigate } from 'react-router'; +import { Wine, MessageSquare, Tag, Image, BookOpen } from 'lucide-react'; import { useAdminAlcoholList } from '@/hooks/useAdminAlcohols'; import { useHelpList } from '@/hooks/useHelps'; +import { useTastingTagList } from '@/hooks/useTastingTags'; +import { useBannerList } from '@/hooks/useBanners'; +import { useCurationList } from '@/hooks/useCurations'; interface StatCardProps { title: string; value: number | string; icon: React.ReactNode; isLoading?: boolean; + href?: string; } -function StatCard({ title, value, icon, isLoading }: StatCardProps) { +function StatCard({ title, value, icon, isLoading, href }: StatCardProps) { + const navigate = useNavigate(); + return ( -
+
navigate(href) : undefined} + >

{title}

{icon}
@@ -36,8 +46,18 @@ export function DashboardPage() { const { data: alcoholData, isLoading: isAlcoholLoading } = useAdminAlcoholList({ size: 1, }); + const { data: tagData, isLoading: isTagLoading } = useTastingTagList({ + size: 1, + }); + const { data: bannerData, isLoading: isBannerLoading } = useBannerList({ + size: 1, + }); + const { data: curationData, isLoading: isCurationLoading } = useCurationList({ + size: 1, + }); const { data: helpData, isLoading: isHelpLoading } = useHelpList({ pageSize: 1, + status: 'WAITING', }); return ( @@ -49,28 +69,41 @@ export function DashboardPage() {

-
+
} isLoading={isAlcoholLoading} + href="/whisky" /> } + isLoading={isTagLoading} + href="/tasting-tags" /> } + isLoading={isBannerLoading} + href="/banners" + /> + } + isLoading={isCurationLoading} + href="/curations" /> } isLoading={isHelpLoading} + href="/inquiries" />
From bb593ab591037d0fcee9e8dda177d0c25bab5dd7 Mon Sep 17 00:00:00 2001 From: hyejj19 Date: Wed, 18 Feb 2026 22:31:01 +0900 Subject: [PATCH 02/14] =?UTF-8?q?refactor:=20=EB=B0=B0=EB=84=88=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20UI=EB=A5=BC=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EB=AA=A9=EB=A1=9D=EA=B3=BC=20=EC=9D=BC=EC=B9=98?= =?UTF-8?q?=EC=8B=9C=ED=82=A4=EA=B3=A0=20StatusToggle=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 배너 목록 이미지 미리보기 컬럼 제거 - 배너 타입 필터를 활성/비활성 필터로 교체 - 배너 목록에 Switch 인라인 상태 토글 추가 (useBannerUpdateStatus 연결) - 배너 순서 변경 로직에 rollback + refetch 추가 (큐레이션과 동일한 패턴) - Switch+Badge 조합을 StatusToggle 공통 컴포넌트로 추출 - CurationList에서 StatusToggle 컴포넌트 사용으로 교체 Co-Authored-By: Claude Sonnet 4.6 --- src/components/common/StatusToggle.tsx | 40 +++++++ src/pages/banners/BannerList.tsx | 147 +++++++++++++++---------- src/pages/curations/CurationList.tsx | 30 ++--- 3 files changed, 133 insertions(+), 84 deletions(-) create mode 100644 src/components/common/StatusToggle.tsx diff --git a/src/components/common/StatusToggle.tsx b/src/components/common/StatusToggle.tsx new file mode 100644 index 0000000..80b798e --- /dev/null +++ b/src/components/common/StatusToggle.tsx @@ -0,0 +1,40 @@ +/** + * 활성/비활성 상태 토글 컴포넌트 + * Switch와 Badge를 조합하여 인라인 상태 변경 UI 제공 + */ + +import { Check, X } from 'lucide-react'; +import { Switch } from '@/components/ui/switch'; +import { Badge } from '@/components/ui/badge'; + +interface StatusToggleProps { + isActive: boolean; + onToggle: () => void; + disabled?: boolean; +} + +export function StatusToggle({ isActive, onToggle, disabled }: StatusToggleProps) { + return ( +
e.stopPropagation()} + > + + {isActive ? ( + + + 활성 + + ) : ( + + + 비활성 + + )} +
+ ); +} diff --git a/src/pages/banners/BannerList.tsx b/src/pages/banners/BannerList.tsx index c8f764c..3d8273b 100644 --- a/src/pages/banners/BannerList.tsx +++ b/src/pages/banners/BannerList.tsx @@ -1,12 +1,13 @@ /** * 배너 목록 페이지 * - URL 쿼리파라미터로 검색/필터/페이지네이션 상태 관리 + * - 인라인 Switch로 활성화 상태 토글 * - 순서 변경 모드에서 드래그 앤 드롭으로 순서 변경 */ import { useState, useEffect } from 'react'; import { useNavigate, useSearchParams } from 'react-router'; -import { Search, ImageOff, Plus, GripVertical, Check, X, ArrowUpDown } from 'lucide-react'; +import { Search, Plus, GripVertical, ArrowUpDown } from 'lucide-react'; import { Input } from '@/components/ui/input'; import { Select, @@ -26,21 +27,18 @@ import { import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Pagination } from '@/components/common/Pagination'; -import { useBannerList, useBannerUpdateSortOrder } from '@/hooks/useBanners'; +import { StatusToggle } from '@/components/common/StatusToggle'; +import { useBannerList, useBannerUpdateStatus, useBannerUpdateSortOrder } from '@/hooks/useBanners'; import { type BannerSearchParams, - type BannerType, type BannerListItem, BANNER_TYPE_LABELS, } from '@/types/api'; -const BANNER_TYPE_OPTIONS: { value: BannerType | 'ALL'; label: string }[] = [ +const IS_ACTIVE_OPTIONS = [ { value: 'ALL', label: '전체' }, - { value: 'SURVEY', label: '설문조사' }, - { value: 'CURATION', label: '큐레이션' }, - { value: 'AD', label: '광고' }, - { value: 'PARTNERSHIP', label: '제휴' }, - { value: 'ETC', label: '기타' }, + { value: 'true', label: '활성' }, + { value: 'false', label: '비활성' }, ]; export function BannerListPage() { @@ -49,7 +47,7 @@ export function BannerListPage() { // URL에서 검색 파라미터 읽기 const keyword = urlParams.get('keyword') ?? ''; - const bannerType = urlParams.get('bannerType') as BannerType | null; + const isActiveParam = urlParams.get('isActive'); const page = Number(urlParams.get('page')) || 0; const size = Number(urlParams.get('size')) || 20; @@ -63,6 +61,9 @@ export function BannerListPage() { const [draggedItem, setDraggedItem] = useState(null); const [dragOverId, setDragOverId] = useState(null); + // 순서 변경 진행 중 상태 + const [isReordering, setIsReordering] = useState(false); + // URL의 keyword가 변경되면 입력 필드도 동기화 useEffect(() => { setKeywordInput(keyword); @@ -71,12 +72,13 @@ export function BannerListPage() { // API 요청용 파라미터 const searchParams: BannerSearchParams = { keyword: keyword || undefined, - bannerType: bannerType || undefined, + isActive: isActiveParam === 'true' ? true : isActiveParam === 'false' ? false : undefined, page, size, }; - const { data, isLoading } = useBannerList(searchParams); + const { data, isLoading, refetch } = useBannerList(searchParams); + const toggleStatusMutation = useBannerUpdateStatus(); const updateSortOrderMutation = useBannerUpdateSortOrder(); // URL 파라미터 업데이트 헬퍼 @@ -111,10 +113,10 @@ export function BannerListPage() { } }; - const handleBannerTypeChange = (value: string) => { + const handleIsActiveChange = (value: string) => { updateUrlParams({ - bannerType: value === 'ALL' ? undefined : value, - page: '0', // 타입 변경 시 첫 페이지로 + isActive: value === 'ALL' ? undefined : value, + page: '0', // 필터 변경 시 첫 페이지로 }); }; @@ -138,6 +140,13 @@ export function BannerListPage() { } }; + const handleStatusToggle = (bannerId: number, currentStatus: boolean) => { + toggleStatusMutation.mutate({ + bannerId, + data: { isActive: !currentStatus }, + }); + }; + // 순서 변경 모드 토글 const toggleReorderMode = () => { setIsReorderMode(!isReorderMode); @@ -166,8 +175,8 @@ export function BannerListPage() { setDragOverId(null); }; - const handleDrop = (e: React.DragEvent, targetItem: BannerListItem) => { - if (!isReorderMode) return; + const handleDrop = async (e: React.DragEvent, targetItem: BannerListItem) => { + if (!isReorderMode || isReordering) return; e.preventDefault(); setDragOverId(null); @@ -186,20 +195,55 @@ export function BannerListPage() { return; } - // 순서 변경 + // 순서 변경 (배열 재정렬) items.splice(draggedIndex, 1); items.splice(targetIndex, 0, draggedItem); - // 변경된 순서로 개별 API 호출 (페이지 오프셋 반영) + // 영향받는 범위 계산 (draggedIndex ~ targetIndex) + const minIndex = Math.min(draggedIndex, targetIndex); + const maxIndex = Math.max(draggedIndex, targetIndex); + const affectedItems = items.slice(minIndex, maxIndex + 1); + + // 롤백을 위한 기존 sortOrder 저장 + const originalSortOrders = new Map( + data.items.map((item) => [item.id, item.sortOrder]) + ); + + // 페이지 오프셋 반영 const pageOffset = page * size; - items.forEach((item, index) => { - const newSortOrder = pageOffset + index; - if (item.sortOrder !== newSortOrder) { - updateSortOrderMutation.mutate({ bannerId: item.id, data: { sortOrder: newSortOrder } }); - } - }); - setDraggedItem(null); + // 각 배너의 sortOrder 업데이트 (순차 호출 + 실패 시 롤백) + setIsReordering(true); + try { + for (let idx = 0; idx < affectedItems.length; idx++) { + const item = affectedItems[idx]!; + await updateSortOrderMutation.mutateAsync({ + bannerId: item.id, + data: { sortOrder: pageOffset + minIndex + idx }, + }); + } + // 목록 새로고침 + await refetch(); + } catch { + // 실패 시 기존 sortOrder로 롤백 시도 + for (const item of affectedItems) { + const originalOrder = originalSortOrders.get(item.id); + if (originalOrder === undefined) continue; + try { + await updateSortOrderMutation.mutateAsync({ + bannerId: item.id, + data: { sortOrder: originalOrder }, + }); + } catch { + // 롤백 중 에러는 무시하고 가능한 한 복구 시도 + } + } + // 롤백 후 목록 새로고침 + await refetch(); + } finally { + setIsReordering(false); + setDraggedItem(null); + } }; const handleDragEnd = () => { @@ -252,6 +296,7 @@ export function BannerListPage() {

순서 변경 모드 - 우측의 핸들을 드래그하여 배너 순서를 변경할 수 있습니다. + {isReordering && (순서 변경 중...)}

)} @@ -269,14 +314,14 @@ export function BannerListPage() { />
+ + +
+ + +
{/* 테이블 */} @@ -171,56 +197,69 @@ export function WhiskyListPage() { 영문명 카테고리 수정일 + {includeDeleted && ( + 상태 + )} {isLoading ? ( - + 로딩 중... ) : data?.items.length === 0 ? ( - + 검색 결과가 없습니다. ) : ( - data?.items.map((item) => ( - handleRowClick(item.alcoholId)} - > - - {item.alcoholId} - - - {item.imageUrl ? ( - {item.korName} - ) : ( -
- -
+ data?.items.map((item) => { + const isDeleted = item.deletedAt != null; + return ( + handleRowClick(item.alcoholId)} + > + + {item.alcoholId} + + + {item.imageUrl ? ( + {item.korName} + ) : ( +
+ +
+ )} +
+ {item.korName} + + {item.engName} + + {item.korCategoryName} + + {new Date(item.modifiedAt).toLocaleDateString('ko-KR')} + + {includeDeleted && ( + + {isDeleted && ( + 삭제됨 + )} + )} -
- {item.korName} - - {item.engName} - - {item.korCategoryName} - - {new Date(item.modifiedAt).toLocaleDateString('ko-KR')} - -
- )) + + ); + }) )}
diff --git a/src/pages/whisky/components/WhiskyBasicInfoCard.tsx b/src/pages/whisky/components/WhiskyBasicInfoCard.tsx index 35fa792..82dbee8 100644 --- a/src/pages/whisky/components/WhiskyBasicInfoCard.tsx +++ b/src/pages/whisky/components/WhiskyBasicInfoCard.tsx @@ -27,6 +27,7 @@ export interface WhiskyBasicInfoCardProps { categories: CategoryReference[]; regions: Array<{ id: number; korName: string }>; distilleries: Array<{ id: number; korName: string }>; + disabled?: boolean; } export function WhiskyBasicInfoCard({ @@ -34,6 +35,7 @@ export function WhiskyBasicInfoCard({ categories, regions, distilleries, + disabled = false, }: WhiskyBasicInfoCardProps) { const { register, watch, setValue, formState } = form; const { errors } = formState; @@ -71,7 +73,7 @@ export function WhiskyBasicInfoCard({ 기본 정보 위스키의 기본 정보를 입력합니다. - + {/* 한글명 / 영문명 */}
diff --git a/src/pages/whisky/components/WhiskyImageCard.tsx b/src/pages/whisky/components/WhiskyImageCard.tsx index 573b5cc..cc271cb 100644 --- a/src/pages/whisky/components/WhiskyImageCard.tsx +++ b/src/pages/whisky/components/WhiskyImageCard.tsx @@ -19,6 +19,7 @@ export interface WhiskyImageCardProps { onImageChange: (file: File | null, previewUrl: string | null) => void; error?: string; isUploading?: boolean; + disabled?: boolean; } export function WhiskyImageCard({ @@ -26,6 +27,7 @@ export function WhiskyImageCard({ onImageChange, error, isUploading = false, + disabled = false, }: WhiskyImageCardProps) { return ( @@ -41,7 +43,7 @@ export function WhiskyImageCard({ 이미지를 드래그하거나 클릭하여 업로드합니다. - + {error &&

{error}

}
diff --git a/src/pages/whisky/components/WhiskyRelatedKeywordsCard.tsx b/src/pages/whisky/components/WhiskyRelatedKeywordsCard.tsx index ec9d2a0..986cfa5 100644 --- a/src/pages/whisky/components/WhiskyRelatedKeywordsCard.tsx +++ b/src/pages/whisky/components/WhiskyRelatedKeywordsCard.tsx @@ -14,11 +14,13 @@ import { TagSelector } from '@/components/common/TagSelector'; export interface WhiskyRelatedKeywordsCardProps { keywords: string[]; onKeywordsChange: (keywords: string[]) => void; + disabled?: boolean; } export function WhiskyRelatedKeywordsCard({ keywords, onKeywordsChange, + disabled = false, }: WhiskyRelatedKeywordsCardProps) { return ( @@ -28,7 +30,7 @@ export function WhiskyRelatedKeywordsCard({ 검색 시 이 위스키를 찾을 수 있도록 연관 키워드를 입력합니다. - + void; + disabled?: boolean; } export function WhiskyTastingTagCard({ tastingTags, availableTags = [], onTagsChange, + disabled = false, }: WhiskyTastingTagCardProps) { const handleTagsChange = (tags: string[]) => { const newTags = tags.map((name) => { @@ -41,7 +43,7 @@ export function WhiskyTastingTagCard({ 이 위스키의 테이스팅 노트를 선택하거나 직접 추가할 수 있습니다. - + tag.korName)} availableTags={availableTags} diff --git a/src/pages/whisky/useWhiskyDetailForm.ts b/src/pages/whisky/useWhiskyDetailForm.ts index 11674ed..bf0bac6 100644 --- a/src/pages/whisky/useWhiskyDetailForm.ts +++ b/src/pages/whisky/useWhiskyDetailForm.ts @@ -47,6 +47,7 @@ export interface UseWhiskyDetailFormReturn { form: ReturnType>; isLoading: boolean; isNewMode: boolean; + isDeleted: boolean; isPending: boolean; whiskyData: ReturnType['data']; categories: CategoryReference[]; @@ -191,10 +192,13 @@ export function useWhiskyDetailForm(id: string | undefined): UseWhiskyDetailForm } }; + const isDeleted = whiskyData?.deletedAt != null; + return { form, isLoading, isNewMode, + isDeleted, isPending: createMutation.isPending || deleteMutation.isPending || updateMutation.isPending, whiskyData, categories: categoryData ?? [], diff --git a/src/test/mocks/data.ts b/src/test/mocks/data.ts index 909b629..f7abbef 100644 --- a/src/test/mocks/data.ts +++ b/src/test/mocks/data.ts @@ -5,6 +5,8 @@ import type { TastingTagDeleteResponse, TastingTagAlcoholConnectionResponse, TastingTagAlcohol, + AlcoholListItem, + AlcoholDeleteResponse, BannerListItem, BannerDetail, BannerCreateResponse, @@ -109,6 +111,53 @@ export const mockAlcoholDisconnectionResponse: TastingTagAlcoholConnectionRespon responseAt: '2024-06-01T00:00:00', }; +// ============================================ +// Alcohol Mock Data +// ============================================ + +export const mockAlcoholListItems: AlcoholListItem[] = [ + { + alcoholId: 10, + korName: '글렌피딕 12년', + engName: 'Glenfiddich 12', + korCategoryName: '싱글몰트', + engCategoryName: 'Single Malt', + imageUrl: 'https://example.com/glenfiddich.jpg', + createdAt: '2024-01-01T00:00:00', + modifiedAt: '2024-06-01T00:00:00', + deletedAt: null, + }, + { + alcoholId: 20, + korName: '맥캘란 18년', + engName: 'Macallan 18', + korCategoryName: '싱글몰트', + engCategoryName: 'Single Malt', + imageUrl: null, + createdAt: '2024-03-01T00:00:00', + modifiedAt: '2024-06-01T00:00:00', + deletedAt: null, + }, + { + alcoholId: 30, + korName: '삭제된 위스키', + engName: 'Deleted Whisky', + korCategoryName: '블렌디드', + engCategoryName: 'Blend', + imageUrl: null, + createdAt: '2024-01-01T00:00:00', + modifiedAt: '2024-05-01T00:00:00', + deletedAt: '2024-07-01T00:00:00', + }, +]; + +export const mockAlcoholDeleteResponse: AlcoholDeleteResponse = { + code: 'ALCOHOL_DELETED', + message: '위스키가 삭제되었습니다.', + targetId: 10, + responseAt: '2024-06-01T00:00:00', +}; + // ============================================ // Banner Mock Data // ============================================ diff --git a/src/test/mocks/handlers.ts b/src/test/mocks/handlers.ts index 03bd556..8c1de21 100644 --- a/src/test/mocks/handlers.ts +++ b/src/test/mocks/handlers.ts @@ -6,6 +6,8 @@ import { mockDeleteResponse, mockAlcoholConnectionResponse, mockAlcoholDisconnectionResponse, + mockAlcoholListItems, + mockAlcoholDeleteResponse, mockBannerListItems, mockBannerDetail, mockBannerCreateResponse, @@ -231,4 +233,60 @@ export const bannerHandlers = [ }), ]; -export const handlers = [...tastingTagHandlers, ...bannerHandlers]; +// ============================================ +// Alcohol Handlers +// ============================================ + +const ALCOHOL_BASE = '/admin/api/v1/alcohols'; + +export const alcoholHandlers = [ + // GET 목록 + http.get(ALCOHOL_BASE, ({ request }) => { + const url = new URL(request.url); + const keyword = url.searchParams.get('keyword'); + const category = url.searchParams.get('category'); + const includeDeleted = url.searchParams.get('includeDeleted'); + const size = Number(url.searchParams.get('size') ?? 20); + const page = Number(url.searchParams.get('page') ?? 0); + + let items = mockAlcoholListItems; + + // 기본: 삭제된 데이터 제외 + if (includeDeleted !== 'true') { + items = items.filter((item) => item.deletedAt === null); + } + + if (keyword) { + items = items.filter( + (item) => + item.korName.includes(keyword) || + item.engName.toLowerCase().includes(keyword.toLowerCase()) + ); + } + if (category) { + items = items.filter((item) => item.engCategoryName.toUpperCase().replace(' ', '_') === category); + } + + return HttpResponse.json( + wrapApiResponse(items, { + page, + size, + totalElements: items.length, + totalPages: Math.ceil(items.length / size), + hasNext: false, + }) + ); + }), + + // DELETE 삭제 + http.delete(`${ALCOHOL_BASE}/:alcoholId`, ({ params }) => { + return HttpResponse.json( + wrapApiResponse({ + ...mockAlcoholDeleteResponse, + targetId: Number(params.alcoholId), + }) + ); + }), +]; + +export const handlers = [...tastingTagHandlers, ...bannerHandlers, ...alcoholHandlers]; diff --git a/src/types/api/alcohol.api.ts b/src/types/api/alcohol.api.ts index 4a873b1..488cedb 100644 --- a/src/types/api/alcohol.api.ts +++ b/src/types/api/alcohol.api.ts @@ -98,6 +98,8 @@ export interface AlcoholApiTypes { page?: number; /** 페이지 크기 (기본값: 20) */ size?: number; + /** 삭제된 데이터 포함 여부 (기본값: false) */ + includeDeleted?: boolean; }; /** 응답 아이템 */ response: { @@ -117,6 +119,8 @@ export interface AlcoholApiTypes { createdAt: string; /** 수정일시 */ modifiedAt: string; + /** 삭제일시 (null이면 미삭제) */ + deletedAt: string | null; }; /** 페이지네이션 메타 정보 */ meta: { @@ -195,6 +199,8 @@ export interface AlcoholApiTypes { createdAt: string; /** 수정일시 */ modifiedAt: string; + /** 삭제일시 (null이면 미삭제) */ + deletedAt: string | null; }; }; /** 술 생성 */ From 2ba8dc39509b4a2abe658459008025924e5ae853 Mon Sep 17 00:00:00 2001 From: hyejj19 Date: Mon, 23 Feb 2026 21:57:13 +0900 Subject: [PATCH 12/14] fix: remove debug console.log statements from WhiskyDetail Addresses review comment by @copilot-pull-request-reviewer on src/pages/whisky/WhiskyDetail.tsx:130. PR: #28 --- src/pages/whisky/WhiskyDetail.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/pages/whisky/WhiskyDetail.tsx b/src/pages/whisky/WhiskyDetail.tsx index 20d510c..9d1cd91 100644 --- a/src/pages/whisky/WhiskyDetail.tsx +++ b/src/pages/whisky/WhiskyDetail.tsx @@ -92,12 +92,8 @@ export function WhiskyDetailPage() { const handleSubmit = form.handleSubmit( (data) => { - console.log('[DEBUG] handleSubmit callback called', data); onSubmit(data, { tastingTags, relatedKeywords, imagePreviewUrl }); }, - (errors) => { - console.log('[DEBUG] handleSubmit validation errors', errors); - } ); const handleDeleteConfirm = () => { @@ -127,7 +123,7 @@ export function WhiskyDetailPage() { 삭제 )} - From 11003a576cbc487383897a9cdc996e9a6f1310ad Mon Sep 17 00:00:00 2001 From: hyejj19 Date: Mon, 23 Feb 2026 21:57:29 +0900 Subject: [PATCH 13/14] fix: guard ImageUpload onImageChange with noop when disabled pointer-events-none only blocks mouse events; keyboard navigation can still trigger file upload. Replace onImageChange with noop when disabled to prevent keyboard-initiated changes. Addresses review comment by @copilot-pull-request-reviewer on src/pages/whisky/components/WhiskyImageCard.tsx:47. PR: #28 --- src/pages/whisky/components/WhiskyImageCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/whisky/components/WhiskyImageCard.tsx b/src/pages/whisky/components/WhiskyImageCard.tsx index cc271cb..aea6b87 100644 --- a/src/pages/whisky/components/WhiskyImageCard.tsx +++ b/src/pages/whisky/components/WhiskyImageCard.tsx @@ -44,7 +44,7 @@ export function WhiskyImageCard({ 이미지를 드래그하거나 클릭하여 업로드합니다. - + {} : onImageChange} /> {error &&

{error}

}
From f7feb58e7005b45829b1adb870d1388b89560d60 Mon Sep 17 00:00:00 2001 From: Hyejung Park Date: Tue, 24 Feb 2026 08:21:42 +0900 Subject: [PATCH 14/14] Bump version from 1.0.3 to 1.1.3 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index e4c0d46..781dcb0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.3 \ No newline at end of file +1.1.3