Skip to content

Commit 088073d

Browse files
authored
Merge pull request #102 from Team-Senifit/fix/selection-persist-hydration
[운동 탭] 이전 선택 상태 유지 안정화
2 parents 36d109c + d67b686 commit 088073d

10 files changed

Lines changed: 74 additions & 39 deletions

File tree

src/app/(with-container)/(exercise)/exercise/customized/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const Page = () => {
3636
setSelectedProgram,
3737
selectedRoutineRecord,
3838
setSelectedRoutineRecord,
39+
hasHydrated,
3940
customizedForm,
4041
setCustomizedForm,
4142
} = useProgramStore();
@@ -65,6 +66,7 @@ const Page = () => {
6566
}, [customizedForm, reset]);
6667

6768
const onSubmit = async (data: ICustomizedRoutineField) => {
69+
if (!hasHydrated) return;
6870
// 체크 페이지에서 돌아왔을 때 옵션 UI 복원을 위해 저장
6971
setCustomizedForm(data);
7072
const {
@@ -89,6 +91,7 @@ const Page = () => {
8991
router.push("/exercise/members");
9092
};
9193

94+
if (!hasHydrated) return null;
9295
return (
9396
<>
9497
<Stack spacing={[2, 3]}>

src/app/(with-container)/(exercise)/exercise/members/page.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const Page = () => {
3333

3434
const {
3535
type,
36+
hasHydrated,
3637
selectedMembers: storedSelectedMembers,
3738
setSelectedMembers,
3839
setSelectedRoutineRecord,
@@ -80,6 +81,9 @@ const Page = () => {
8081

8182
const selectedMembers = watch("members");
8283

84+
// rehydrate 전엔 이전 선택이 비어 보일 수 있어, 실수로 '다음'을 눌러 선택이 초기화되는 문제를 방지
85+
if (!hasHydrated) return null;
86+
8387
const onSubmit = (data: { members: number[] }) => {
8488
if (!data.members?.length) {
8589
setSelectedMembers([]);

src/app/(with-container)/(exercise)/exercise/thematic/page.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const Page = () => {
3535

3636
const {
3737
setSelectedRoutineRecord,
38+
hasHydrated,
3839
thematicWorkoutKind,
3940
setThematicWorkoutKind,
4041
} = useProgramStore();
@@ -56,6 +57,7 @@ const Page = () => {
5657
}, [thematicWorkoutKind, reset]);
5758

5859
const onSubmit = (data: IThematicRoutineField) => {
60+
if (!hasHydrated) return;
5961
const workoutKind = data.workout_kind;
6062
setThematicWorkoutKind(workoutKind);
6163

@@ -85,6 +87,8 @@ const Page = () => {
8587

8688
router.push(`/exercise/thematic/${workoutKind}`);
8789
};
90+
91+
if (!hasHydrated) return null;
8892
return (
8993
<>
9094
<Stack

src/app/panel/SenifitThemeProvider.tsx

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -100,25 +100,24 @@ declare module "@mui/material/styles" {
100100
};
101101
}
102102

103-
interface PaletteOptions
104-
extends Pick<
105-
MuiPaletteOptions,
106-
| "common"
107-
| "primary"
108-
| "secondary"
109-
| "error"
110-
| "warning"
111-
| "info"
112-
| "success"
113-
| "mode"
114-
| "contrastThreshold"
115-
| "tonalOffset"
116-
| "divider"
117-
| "background"
118-
| "text"
119-
| "action"
120-
| "grey"
121-
> {
103+
interface PaletteOptions extends Pick<
104+
MuiPaletteOptions,
105+
| "common"
106+
| "primary"
107+
| "secondary"
108+
| "error"
109+
| "warning"
110+
| "info"
111+
| "success"
112+
| "mode"
113+
| "contrastThreshold"
114+
| "tonalOffset"
115+
| "divider"
116+
| "background"
117+
| "text"
118+
| "action"
119+
| "grey"
120+
> {
122121
static?: {
123122
white?: string;
124123
black?: string;

src/components/CTAButton.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Button, ButtonProps, Typography } from "@mui/material";
22
import React from "react";
3+
import Link from "next/link";
34

45
// 이 컴포넌트에 종속되는 타입이므로 이 파일에 정의
56
export interface ICTAButtonProps extends ButtonProps {
@@ -8,18 +9,25 @@ export interface ICTAButtonProps extends ButtonProps {
89
}
910

1011
const CTAButton = (props: ICTAButtonProps) => {
12+
const { href, text, ...rest } = props;
1113
return (
1214
<Button
1315
variant={"text"}
14-
{...props}
16+
{...rest}
17+
{...(href
18+
? {
19+
component: Link,
20+
href,
21+
}
22+
: {})}
1523
sx={{
1624
py: 2,
1725
px: 8,
1826
borderRadius: "0.75rem",
1927
...props.sx,
2028
}}
2129
>
22-
<Typography variant={"Heading1"}>{props.text}</Typography>
30+
<Typography variant={"Heading1"}>{text}</Typography>
2331
</Button>
2432
);
2533
};

src/components/SenifitToggleButtonGroupField.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ export interface ISenifitToggleButtonGroupFieldProps<
1414
T extends string | number | boolean,
1515
TFieldValues extends FieldValues = FieldValues,
1616
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
17-
> extends Omit<ISenifitToggleButtonGroupProps<T>, "value" | "onChange">,
17+
>
18+
extends
19+
Omit<ISenifitToggleButtonGroupProps<T>, "value" | "onChange">,
1820
Pick<
1921
UseControllerProps<TFieldValues, TName>,
2022
"name" | "control" | "rules"

src/components/VideoPlayer.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ export interface IVideoHandle {
2222
getEl: () => HTMLVideoElement | null;
2323
}
2424

25-
export interface IVideoPlayerProps
26-
extends Omit<React.VideoHTMLAttributes<HTMLVideoElement>, "onTimeUpdate"> {
25+
export interface IVideoPlayerProps extends Omit<
26+
React.VideoHTMLAttributes<HTMLVideoElement>,
27+
"onTimeUpdate"
28+
> {
2729
src: string;
2830
length: number;
2931
poster?: string;

src/states/useProgramStore.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { create } from "zustand";
66
import { persist, createJSONStorage } from "zustand/middleware";
77

88
interface IProgramStore {
9+
hasHydrated: boolean;
910
selectedProgram: IRoutineDetail | null;
1011
selectedMembers: IMember[];
1112
type: "customized" | "popular" | ["thematic", WorkoutKind] | null;
@@ -29,18 +30,21 @@ interface IProgramStore {
2930
) => void;
3031
setThematicWorkoutKind: (kind: WorkoutKind | null) => void;
3132
setCustomizedForm: (form: Partial<ICustomizedRoutineField> | null) => void;
33+
setHasHydrated: (v: boolean) => void;
3234
clearStore: () => void;
3335
}
3436

3537
const useProgramStore = create<IProgramStore>()(
3638
persist(
3739
(set) => ({
40+
hasHydrated: false,
3841
selectedProgram: null,
3942
selectedMembers: [],
4043
type: null,
4144
selectedRoutineRecord: null,
4245
thematicWorkoutKind: null,
4346
customizedForm: null,
47+
setHasHydrated: (v) => set({ hasHydrated: v }),
4448
setSelectedRoutineRecord: (record) =>
4549
set({ selectedRoutineRecord: record }),
4650
setSelectedProgram: (program) => set({ selectedProgram: program }),
@@ -50,6 +54,7 @@ const useProgramStore = create<IProgramStore>()(
5054
setCustomizedForm: (form) => set({ customizedForm: form }),
5155
clearStore: () =>
5256
set({
57+
hasHydrated: true, // clear 시에도 UI 입력 차단은 하지 않음
5358
selectedProgram: null,
5459
selectedMembers: [],
5560
type: null,
@@ -61,6 +66,9 @@ const useProgramStore = create<IProgramStore>()(
6166
{
6267
name: "program-store",
6368
storage: createJSONStorage(() => localStorage),
69+
onRehydrateStorage: () => (state) => {
70+
state?.setHasHydrated(true);
71+
},
6472
},
6573
),
6674
);

src/types/ISelect.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,10 @@ export interface ISelectOption<T extends TOptionValue> {
1818
}
1919

2020
/** 공용 Select 베이스 props */
21-
export interface IBaseSelectProps<T extends TOptionValue>
22-
extends Omit<
23-
SelectProps<T>,
24-
"multiple" | "value" | "onChange" | "renderValue" | "native"
25-
> {
21+
export interface IBaseSelectProps<T extends TOptionValue> extends Omit<
22+
SelectProps<T>,
23+
"multiple" | "value" | "onChange" | "renderValue" | "native"
24+
> {
2625
options: ISelectOption<T>[];
2726
placeholder?: ReactNode;
2827
/** 메뉴 Paper / List 개별 커스텀 */
@@ -31,16 +30,18 @@ export interface IBaseSelectProps<T extends TOptionValue>
3130
}
3231

3332
/** 단일 선택 */
34-
export interface ISingleSelectProps<T extends TOptionValue>
35-
extends IBaseSelectProps<T> {
33+
export interface ISingleSelectProps<
34+
T extends TOptionValue,
35+
> extends IBaseSelectProps<T> {
3636
multiple?: false;
3737
value: T | "";
3838
onChange: (v: T) => void;
3939
}
4040

4141
/** 다중 선택 */
42-
export interface IMultiSelectProps<T extends TOptionValue>
43-
extends IBaseSelectProps<T> {
42+
export interface IMultiSelectProps<
43+
T extends TOptionValue,
44+
> extends IBaseSelectProps<T> {
4445
multiple: true;
4546
value: T[];
4647
onChange: (v: T[]) => void;

src/types/IToggleButton.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ export interface IResponsiveMaxItems {
1212
}
1313

1414
/** 개별 토글 버튼 공용 props */
15-
export interface ISenifitToggleButtonProps
16-
extends Omit<MuiButtonProps, "onChange"> {
15+
export interface ISenifitToggleButtonProps extends Omit<
16+
MuiButtonProps,
17+
"onChange"
18+
> {
1719
/** 버튼 높이 프리셋 */
1820
sizeVariant?: SizeVariant;
1921
/** 그룹 내에서 각 버튼을 가변 폭(flex:1)으로 확장할지 여부 */
@@ -68,8 +70,9 @@ export interface IBaseProps<T extends string | number | boolean> {
6870
}
6971

7072
/** 단일 선택(Exclusive) 모드용 props */
71-
export interface IExclusiveProps<T extends string | number | boolean>
72-
extends IBaseProps<T> {
73+
export interface IExclusiveProps<
74+
T extends string | number | boolean,
75+
> extends IBaseProps<T> {
7376
/** true 또는 생략 시 단일 선택 모드 */
7477
exclusive?: true;
7578
/** 현재 선택 값 */
@@ -79,8 +82,9 @@ export interface IExclusiveProps<T extends string | number | boolean>
7982
}
8083

8184
/** 다중 선택(Multi) 모드용 props */
82-
export interface IMultiProps<T extends string | number | boolean>
83-
extends IBaseProps<T> {
85+
export interface IMultiProps<
86+
T extends string | number | boolean,
87+
> extends IBaseProps<T> {
8488
/** false일 때만 다중 선택 모드로 동작 */
8589
exclusive: false;
8690
/** 현재 선택 값 배열 */

0 commit comments

Comments
 (0)