Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
5b56183
feat: EditCardContext에 대시보드 카드 드래그앤드랍 상태 관리 추가
lwjmcn Feb 15, 2026
f709a51
chore: import 경로 단축
lwjmcn Feb 15, 2026
8f126d8
feat: EditCardContext에 픽셀 좌표 계산을 위한 gridRef 추가
lwjmcn Feb 15, 2026
bf00cc2
refactor: 카드 상태 2차원 그리드 대신 카드 배열 기준으로 리팩터링
lwjmcn Feb 15, 2026
fa99942
feat: 그리드 셀 크기 계산을 위한 useGridCellSize 추가
lwjmcn Feb 15, 2026
3e281f5
feat: MiniViewActiveCard의 props 수정 (posX, posY -> colNo, rowNo)
lwjmcn Feb 15, 2026
ae9de0d
feat: 카드편집 뷰 카드 클릭 시 추가 이벤트 동작 및 클릭 이벤트 전파 방지
lwjmcn Feb 15, 2026
6b3a8d2
fix: parameter row,col 순서 변경 버그
lwjmcn Feb 15, 2026
d2103e0
feat: 드래그 앤 드롭 카드 기능 추가 및 카드 밀어내기 재귀 알고리즘 구현
lwjmcn Feb 15, 2026
fc3937f
feat: 카드 편집 및 미니 뷰에 드래그 앤 드롭 핸들러 연결 및 ghost 컴포넌트 추가
lwjmcn Feb 15, 2026
966d071
fix: 이미지 드래그 막기
lwjmcn Feb 15, 2026
e862450
fix: 이미 추가된 카드 드래그 시도 시 텍스트가 선택되는 문제
lwjmcn Feb 15, 2026
c17a05f
feat: dragOver 이벤트 핸들러에 스로틀링 추가
lwjmcn Feb 15, 2026
8077dfb
chore: 메트릭 이름 오타 수정
lwjmcn Feb 15, 2026
ca7e601
style: 메트릭 이름 줄바꿈 스타일링
lwjmcn Feb 15, 2026
bb21461
style: 드래그앤드랍 고스트 스타일링
lwjmcn Feb 15, 2026
57ae2c6
feat: 대시보드 목록 및 카드 레이아웃 조회 API 및 타입 정의 추가
lwjmcn Feb 17, 2026
28ec3f3
feat: 대시보드 목록 및 카드 조회를 위한 query option 정의
lwjmcn Feb 17, 2026
b9bff7a
feat: 대시보드 이름 수정 요청 API 및 타입 정의 추가
lwjmcn Feb 17, 2026
2042839
feat: 대시보드 삭제 API 및 타입 정의 추가
lwjmcn Feb 17, 2026
5048799
feat: 대시보드 탭 추가 API 및 타입 정의 추가
lwjmcn Feb 17, 2026
cf01d26
feat: 대시보드 카드 레이아웃 업데이트 API 및 타입 정의 추가
lwjmcn Feb 17, 2026
97548f8
chore: dto, query 타입 배럴 파일 추가
lwjmcn Feb 17, 2026
75fda4c
refactor: 대시보드 타입 dto에서 일반 타입으로 분리
lwjmcn Feb 17, 2026
897835e
feat: 대시보드 API 배럴 파일 추가 및 함수명 변경
lwjmcn Feb 17, 2026
09bbc71
feat: 대시보드 query option명 변경
lwjmcn Feb 17, 2026
1c160e8
feat: 대시보드 context에서 서버 상태 삭제 및 대시보드 목록 query 패치
lwjmcn Feb 17, 2026
c04c223
feat: 대시보드 탭 및 카드 목록 조회 API 연동
lwjmcn Feb 17, 2026
994befe
feat: 대시보드 페이지에 FetchBoundary 추가
lwjmcn Feb 17, 2026
93c29f0
fix: import 경로 단축
lwjmcn Feb 17, 2026
918f7aa
style: fallback이 전체 화면의 중앙에 뜨도록 스타일 변경
lwjmcn Feb 17, 2026
c15a7bd
feat: 대시보드 탭 추가/삭제/편집 mutation 기능 추가
lwjmcn Feb 17, 2026
f89ac3f
feat: shadcn skeleton 컴포넌트 추가
lwjmcn Feb 17, 2026
bd00d83
feat: 대시보드 메인 카드 레이아웃 suspense 추가
lwjmcn Feb 17, 2026
3b53f05
feat: 기본 대시보드 isDefault 값으로 초기화
lwjmcn Feb 17, 2026
d66369d
style: 불필요한 스타일 삭제
lwjmcn Feb 17, 2026
c2faa0a
chore: 불필요한 undefined 처리 삭제
lwjmcn Feb 17, 2026
59036be
fix: Promise.all에 undefined가 아닌 resolve 전달
lwjmcn Feb 17, 2026
d9b7fc1
fix: options에 잘못된 key 사용
lwjmcn Feb 17, 2026
2f90800
refactor: Array -> []로 코드 스타일 통일
lwjmcn Feb 17, 2026
b757428
chore: import 경로 단축
lwjmcn Feb 17, 2026
3b89bc4
Merge branch 'feature/#54-fe-dashboard-edit-draggable' of https://git…
lwjmcn Feb 17, 2026
83d3d79
feat: error fallback에서 에러 메시지 띄우기
lwjmcn Feb 17, 2026
4b6a1e8
feat: 대시보드 카드 목록 조회 및 url 오류 방어 로직 추가
lwjmcn Feb 17, 2026
df30d59
chore: key에서 불필요한 params 삭제
lwjmcn Feb 17, 2026
b4bf1c0
feat: URL에 따른 현재 대시보드 id 컨텍스트에 추가
lwjmcn Feb 17, 2026
8facd4d
feat: 카드 목록 저장 기능 뮤테이션으로 구현
lwjmcn Feb 17, 2026
4e8a8fd
feat: 홈 대시보드에서 카드 편집 비활성화
lwjmcn Feb 17, 2026
94cd315
feat: error fallback 공통 컴포넌트로 분리
lwjmcn Feb 17, 2026
8cdbf4f
feat: 잘못된 대시보드 접근 시 나가기 버튼 노출
lwjmcn Feb 17, 2026
6355759
feat: local storage에 현재 대시보드 id 저장
lwjmcn Feb 17, 2026
17a96ae
feat: 현재 대시보드 정보 local storage에 저장 기능 및 storage 키 상수로 분리
lwjmcn Feb 17, 2026
a40d86d
Merge branch 'develop' into feature/#50-dashboard-edit-api
lwjmcn Feb 20, 2026
52c2938
style: 미니 뷰 크기 변경 및 empty 뷰 outline으로 숨김
lwjmcn Feb 20, 2026
676bbfb
Merge branch 'develop' into feature/#50-dashboard-edit-api
lwjmcn Feb 20, 2026
2e2bab2
fix: merge conflict 해결
lwjmcn Feb 20, 2026
d707552
fix: local storage 에러 시 error boundary 대신 콘솔 로그로민 표시
lwjmcn Feb 20, 2026
40d7027
fix: onClick 비활성화 시 onClick 핸들러 undefined 처리
lwjmcn Feb 20, 2026
d9b2416
refactor: tabs를 매번 find 하는 대신 currentDashboard 객체를 정의
lwjmcn Feb 20, 2026
50ecdaa
refactor: 매직 넘버 교체
lwjmcn Feb 20, 2026
9535789
refactor: 불필요한 useErrrorBoundary 제거
lwjmcn Feb 20, 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
32 changes: 29 additions & 3 deletions frontend/src/components/dashboard/dashboard-edit/CardEditView.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,53 @@
import { useNavigate } from 'react-router-dom';

import { useMutation, useQueryClient } from '@tanstack/react-query';

import { ButtonGroup } from '@/components/shared';
import { useDragAndDropCard, useEditCard } from '@/hooks/dashboard';
import { useEditCardContext } from '@/hooks/dashboard/useEditCardContext';
import { dashboardOptions, putDashboardCardList } from '@/services/dashboard';
import type {
PutDashboardCardListParam,
PutDashboardCardListRequestDto,
} from '@/types/dashboard';

import { CardEditViewTabs } from './CardEditViewTabs';

export const CardEditView = () => {
const navigate = useNavigate();
const queryClient = useQueryClient();

const { isDirty } = useEditCard();

const { isOverList } = useEditCardContext();
const { isOverList, placedCards, dashboardId } = useEditCardContext();

const { handleListDragEnter, handleListDragLeave, handleListDrop } =
useDragAndDropCard();

const mutateCardList = useMutation({
mutationFn: ({
param,
body,
}: {
param: PutDashboardCardListParam;
body: PutDashboardCardListRequestDto;
}) => putDashboardCardList(param, body),
});

const handleCancel = () => {
navigate(-1);
};

const handleSave = () => {
// TODO 저장 로직 구현
navigate(-1);
mutateCardList.mutate(
{ param: { dashboardId }, body: placedCards },
{
onSuccess: () => {
queryClient.invalidateQueries(dashboardOptions.cardList(dashboardId));
navigate(-1);
},
},
);
};

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export const CardEditViewCard = ({ cardCode }: CardEditViewCardProps) => {
}
onDragEnd={handleDragEnd}
className="translate-x-0 cursor-grab active:cursor-grabbing"
onClick={handleAddCard}
onClick={!memoisedIsAdded ? handleAddCard : undefined}
>
<EditCardWrapper
isAdded={memoisedIsAdded}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const CardEditViewTabContentItem = memo(
({ items }: CardEditViewTabContentItemProps) => {
return (
<li>
<h3 className="body-medium-semibold text-grey-800 bg-grey-200 rounded-150 mt-5 mb-3.75 px-300 py-150">
<h3 className="body-medium-semibold text-grey-800 bg-grey-200 rounded-150 mt-6 mb-3 px-300 py-150">
{items.label}
</h3>
<ul className="grid grid-cols-3 gap-5">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
import { ErrorBoundary } from 'react-error-boundary';

import { FetchBoundary } from '@/components/shared';

import { CardEditView } from './CardEditView';
import { EditCardProvider } from './EditCardProvider';
import { EditErrorFallback } from './EditErrorFallback';
import { MiniView } from './MiniView';

export const DashboardEditLayout = () => {
return (
<div className="flex size-full">
<EditCardProvider>
<MiniView />
<CardEditView />
</EditCardProvider>
<div className="flex size-full overflow-y-hidden">
<FetchBoundary>
<ErrorBoundary
fallbackRender={(props) => <EditErrorFallback {...props} />}
>
<EditCardProvider>
<MiniView />
<CardEditView />
</EditCardProvider>
</ErrorBoundary>
</FetchBoundary>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
import { type PropsWithChildren, useRef, useState } from 'react';

import { useSuspenseQuery } from '@tanstack/react-query';

import { EditCardContext } from '@/constants/dashboard/editCardContext';
import { dashboardOptions } from '@/services/dashboard';
import type { DashboardCard, DragState, GhostState } from '@/types/dashboard';

export const EditCardProvider = ({ children }: PropsWithChildren) => {
// TODO: 초기 그리드 상태 서버에서 받아오기
const initPlacedCards: DashboardCard[] = [];
const { data: tabs } = useSuspenseQuery(dashboardOptions.list);

const searchParams = new URLSearchParams(window.location.search);
const dashboardName = searchParams.get('tab') ?? undefined;
const currentDashboard = tabs.find((tab) => tab.name === dashboardName);

if (!currentDashboard) {
throw new Error('존재하지 않는 대시보드입니다.');
} else if (currentDashboard.isDefault) {
throw new Error('기본 대시보드는 편집할 수 없습니다.');
}

const { data: cardList } = useSuspenseQuery(
dashboardOptions.cardList(currentDashboard.id),
);

const initPlacedCards: DashboardCard[] = cardList;

// 카드 그리드 상태
const [placedCards, setPlacedCards] =
Expand All @@ -22,6 +40,7 @@ export const EditCardProvider = ({ children }: PropsWithChildren) => {
return (
<EditCardContext.Provider
value={{
dashboardId: currentDashboard.id,
initPlacedCards,
placedCards,
setPlacedCards,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { FallbackProps } from 'react-error-boundary';
import { useNavigate } from 'react-router-dom';

import { ErrorFallback } from '@/components/shared';

export const EditErrorFallback = (props: FallbackProps) => {
const navigate = useNavigate();

return (
<ErrorFallback
buttonText="나가기"
onClickButton={() => navigate(-1)}
{...props}
/>
);
};
4 changes: 2 additions & 2 deletions frontend/src/components/dashboard/dashboard-edit/MiniView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ export const MiniView = () => {
<h1 className="title-large-bold text-grey-900">{title}</h1>
</header>
<div
className="relative mx-auto min-h-181 min-w-137.5"
className="relative mx-auto min-h-[410.4px] min-w-153"
ref={gridRef}
onDragOver={handleGridDragOver}
onDragLeave={handleGridDragLeave}
onDrop={handleGridDrop}
>
{/* 그리드 셀 가이드라인 */}
<div className="grid h-full grow grid-cols-3 grid-rows-3 gap-5">
<div className="grid h-full grow grid-cols-3 grid-rows-3 gap-3">
{Array.from({ length: GRID_ROW_SIZE * GRID_COL_SIZE }).map(
(_, index) => (
<MiniViewEmptyCard key={`grid-${index}`} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export const MiniViewActiveCard = ({
<img
src={`${CDN_BASE_URL}/assets/images/${type}.svg`}
alt={`${label} 미니 뷰`}
className="size-10"
draggable={false}
/>
<p className="body-small-medium text-grey-900 mt-200 mb-100 text-center break-keep">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { memo } from 'react';

export const MiniViewEmptyCard = memo(() => {
return (
<div className="rounded-400 border-grey-500 border-[1.5px] border-dashed" />
<div className="rounded-400 outline-grey-500 outline-1 -outline-offset-3 outline-dashed" />
);
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useMemo } from 'react';
import { Link } from 'react-router-dom';

import { useSuspenseQuery } from '@tanstack/react-query';
Expand All @@ -6,20 +7,29 @@ import CardsIcon from '@/assets/icons/cards.svg?react';
import { ROUTE_PATHS } from '@/constants/shared';
import { useDashboardTabsContext } from '@/hooks/dashboard';
import { dashboardOptions } from '@/services/dashboard/options';
import { cn } from '@/utils/shared';

export const DashboardEditButton = () => {
const { data: tabs } = useSuspenseQuery(dashboardOptions.list);

const { currentDashboardId } = useDashboardTabsContext();

const disabled = useMemo(() => {
return tabs.find((tab) => tab.isDefault)?.id === currentDashboardId;
}, [tabs, currentDashboardId]);

return (
<Link
to={{
pathname: ROUTE_PATHS.DASHBOARD.EDIT,
search: `?tab=${tabs.find((tab) => tab.id === currentDashboardId)?.name}`,
}}
aria-label="현재 탭의 지표카드 편집"
className="bg-grey-0 text-grey-700 body-medium-medium rounded-200 flex w-fit gap-200 border-none p-300 pl-250 shadow-none"
className={cn(
'bg-grey-0 text-grey-700 body-medium-medium rounded-200 flex w-fit gap-200 border-none p-300 pl-250 shadow-none',
disabled && 'cursor-default opacity-50',
)}
onClick={(e) => disabled && e.preventDefault()}
>
<CardsIcon className="size-5" />
카드 편집
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Skeleton } from '@/components/shared/shadcn-ui';
import { GRID_COL_SIZE, GRID_ROW_SIZE } from '@/constants/dashboard';

export const DashboardMainSuspense = () => {
return (
<div className="mb-10 grid h-181 w-full grid-cols-3 grid-rows-3 gap-5">
{Array.from({ length: 9 }).map((_, index) => {
{Array.from({ length: GRID_COL_SIZE * GRID_ROW_SIZE }).map((_, index) => {
return (
<Skeleton
key={`dashboard-card-${index}`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import {
TooltipTrigger,
} from '@/components/shared/shadcn-ui';
import { DASHBOARD_TABS_DIALOG_MODE } from '@/constants/dashboard';
import { LOCAL_STORAGE_KEY } from '@/constants/shared/localStorageKey';
import { useDashboardTabsContext } from '@/hooks/dashboard';
import { dashboardOptions } from '@/services/dashboard/options';

const TOOLTIP_DISMISSED_KEY = 'dashboard_add_tooltip_dismissed_v1';
const { TOOLTIP_DISMISSED: storageKey } = LOCAL_STORAGE_KEY;
const TWELVE_HOURS_MS = 12 * 60 * 60 * 1000;

const firstActiveDatetime = new Date(); // mocked
Expand All @@ -27,11 +28,11 @@ export const AddTabDialogTrigger = () => {
const [showTooltip, setShowTooltip] = useState(
tabs.length === 1 &&
new Date().getTime() - firstActiveDatetime.getTime() <= TWELVE_HOURS_MS &&
!localStorage.getItem(TOOLTIP_DISMISSED_KEY),
!localStorage.getItem(storageKey),
);

const dismissPermanently = useCallback(() => {
localStorage.setItem(TOOLTIP_DISMISSED_KEY, '1');
localStorage.setItem(storageKey, '1');
setShowTooltip(false);
}, []);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,43 @@ import {
DashboardTabsContext,
type DashboardTabsDialogMode,
} from '@/constants/dashboard';
import { LOCAL_STORAGE_KEY } from '@/constants/shared';
import { dashboardOptions } from '@/services/dashboard';

export const DashboardTabsProvider = ({ children }: PropsWithChildren) => {
const { data: tabs } = useSuspenseQuery(dashboardOptions.list);

const [currentDashboardId, setCurrentDashboardId] = useState<number>(
() => tabs.find((tab) => tab.isDefault)?.id ?? 1,
// localStorage에 대시보드 ID 저장
const { CURRENT_DASHBOARD_ID: storageKey } = LOCAL_STORAGE_KEY;

const getSavedDashboardId = () => {
if (localStorage.getItem(storageKey)) {
const parsedId = Number(localStorage.getItem(storageKey));
if (tabs.some((t) => t.id === parsedId)) {
return parsedId;
}
}
};
const saveDashboardId = (id: number) => {
try {
localStorage.setItem(storageKey, String(id));
} catch (e) {
if (e instanceof Error) {
console.error(e.message);
} else {
console.error('선택한 대시보드 정보를 저장할 수 없어요.', e);
}
}
};

const [currentDashboardId, setCurrentDashboardIdState] = useState<number>(
getSavedDashboardId() ?? tabs.find((tab) => tab.isDefault)?.id ?? 1,
);
const setCurrentDashboardId = (id: number) => {
setCurrentDashboardIdState(id);
saveDashboardId(id);
};

const [dialogState, setDialogState] = useState<{
open: boolean;
mode: DashboardTabsDialogMode | null;
Expand Down
39 changes: 39 additions & 0 deletions frontend/src/components/shared/error-fallback/ErrorFallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { type FallbackProps } from 'react-error-boundary';

import { Button } from '@/components/shared/shadcn-ui';

interface ErrorFallbackProps extends FallbackProps {
errorMessage?: string;
buttonText?: string;
onClickButton?: () => void;
}
export const ErrorFallback = ({
error,
errorMessage,
buttonText = '다시 시도',
onClickButton,
resetErrorBoundary,
}: ErrorFallbackProps) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

p5] 기존 코드에서 에러 메시지랑, 버튼 클릭 헨들러를 props로 추가로 받으셨네요! 범용성이 더 좋아진 것 같습니다 굳굳굳!

const handleClick = () => {
resetErrorBoundary(); // 에러 바운더리의 에러상태 초기화 -> 이걸 해야 에러 바운더리가 다시 자식 컴포넌트를 렌더링 시도함
onClickButton?.();
};

const message =
error instanceof Error ? error.message : '오류가 발생했습니다.';

return (
<div className="flex h-full w-full flex-col items-center justify-center gap-3">
<p className="body-medium-medium text-center">
{errorMessage ?? message}
</p>

<Button
className="rounded-200 body-medium-medium bg-brand-main text-grey-50 px-4 py-3"
onClick={handleClick}
>
{buttonText}
</Button>
</div>
);
};
1 change: 1 addition & 0 deletions frontend/src/components/shared/error-fallback/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ErrorFallback } from './ErrorFallback';
25 changes: 0 additions & 25 deletions frontend/src/components/shared/fetch-boundary/ErrorFallback.tsx

This file was deleted.

Loading