Skip to content

Commit e8c66e5

Browse files
authored
Onboarding 중복 클릭 방지 가드 추가 (#43)
* use .env.dev for gen * feat: 중복 클릭 방지 가드 추가 - 다음 버튼 API 호출 시 중복 클릭 방지 - 기간 선택 버튼들 중복 클릭 방지 - 달력 날짜 선택 중복 클릭 방지 - 달력 월 네비게이션 중복 클릭 방지 (200ms 딜레이) - 제출 중 로딩 상태 표시 ("처리 중...") - 에러 발생 시 상태 복구로 재시도 가능
1 parent 89992cf commit e8c66e5

File tree

2 files changed

+34
-11
lines changed

2 files changed

+34
-11
lines changed

app/onboarding/_components/PeriodSelectionScreen.tsx

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export default function PeriodSelectionScreen({
2929

3030
const [currentMonth, setCurrentMonth] = useState(new Date().getMonth());
3131
const [currentYear, setCurrentYear] = useState(new Date().getFullYear());
32+
const [isSubmitting, setIsSubmitting] = useState(false);
33+
const [isNavigating, setIsNavigating] = useState(false);
3234

3335
const months = [
3436
"1월",
@@ -54,6 +56,16 @@ export default function PeriodSelectionScreen({
5456
return new Date(year, month, 1).getDay();
5557
};
5658

59+
const handleDateSelect = (day: number) => {
60+
if (isSubmitting) return;
61+
setTargetDate(new Date(currentYear, currentMonth, day));
62+
};
63+
64+
const handlePeriodTypeSelect = (type: "months" | "date") => {
65+
if (isSubmitting) return;
66+
setPeriodType(type);
67+
};
68+
5769
const renderCalendar = () => {
5870
const daysInMonth = getDaysInMonth(currentMonth, currentYear);
5971
const firstDay = getFirstDayOfMonth(currentMonth, currentYear);
@@ -95,10 +107,8 @@ export default function PeriodSelectionScreen({
95107
days.push(
96108
<button
97109
key={day}
98-
onClick={() =>
99-
!isPast && setTargetDate(new Date(currentYear, currentMonth, day))
100-
}
101-
disabled={isPast}
110+
onClick={() => !isPast && handleDateSelect(day)}
111+
disabled={isPast || isSubmitting}
102112
className={`h-10 w-10 rounded flex items-center justify-center text-xs font-medium ${isSelected
103113
? "bg-label-primary text-background-alternative"
104114
: isPast
@@ -115,6 +125,10 @@ export default function PeriodSelectionScreen({
115125
};
116126

117127
const navigateMonth = (direction: "prev" | "next") => {
128+
if (isNavigating || isSubmitting) return;
129+
130+
setIsNavigating(true);
131+
setTimeout(() => setIsNavigating(false), 200);
118132
if (direction === "prev") {
119133
if (currentMonth === 0) {
120134
setCurrentMonth(11);
@@ -132,7 +146,7 @@ export default function PeriodSelectionScreen({
132146
}
133147
};
134148

135-
const isNextEnabled = periodType === "months" || targetDate !== null;
149+
const isNextEnabled = (periodType === "months" || targetDate !== null) && !isSubmitting;
136150

137151
return (
138152
<div className="min-h-screen bg-background-alternative flex flex-col">
@@ -154,20 +168,22 @@ export default function PeriodSelectionScreen({
154168
<div className="bg-background-normal rounded-full p-1 mb-8">
155169
<div className="flex">
156170
<button
157-
onClick={() => setPeriodType("months")}
171+
onClick={() => handlePeriodTypeSelect("months")}
172+
disabled={isSubmitting}
158173
className={`flex-1 py-2 px-4 rounded-full font-bold text-sm ${periodType === "months"
159174
? "bg-label-primary text-background-alternative"
160175
: "text-label-alternative"
161-
}`}
176+
} ${isSubmitting ? "opacity-50 cursor-not-allowed" : ""}`}
162177
>
163178
개월 수로 설정
164179
</button>
165180
<button
166-
onClick={() => setPeriodType("date")}
181+
onClick={() => handlePeriodTypeSelect("date")}
182+
disabled={isSubmitting}
167183
className={`flex-1 py-2 px-4 rounded-full font-bold text-sm ${periodType === "date"
168184
? "bg-label-primary text-background-alternative"
169185
: "text-label-alternative"
170-
}`}
186+
} ${isSubmitting ? "opacity-50 cursor-not-allowed" : ""}`}
171187
>
172188
완료 날짜로 설정
173189
</button>
@@ -201,6 +217,8 @@ export default function PeriodSelectionScreen({
201217
onClick={() => navigateMonth("prev")}
202218
className="w-8 h-8 flex items-center justify-center"
203219
disabled={
220+
isNavigating ||
221+
isSubmitting ||
204222
currentMonth === new Date().getMonth() &&
205223
currentYear === new Date().getFullYear()
206224
}
@@ -210,6 +228,7 @@ export default function PeriodSelectionScreen({
210228
<button
211229
onClick={() => navigateMonth("next")}
212230
className="w-8 h-8 flex items-center justify-center"
231+
disabled={isNavigating || isSubmitting}
213232
>
214233
<ChevronLeftIcon className="w-4 h-4 text-label-normal rotate-180" />
215234
</button>
@@ -240,6 +259,9 @@ export default function PeriodSelectionScreen({
240259
<div className="px-4 pb-14">
241260
<ButtonRound
242261
onClick={async () => {
262+
if (isSubmitting) return;
263+
264+
setIsSubmitting(true);
243265
const isPeriodByMonth = periodType === "months";
244266
const dueDate = targetDate
245267
? targetDate.toISOString().split("T")[0]
@@ -260,12 +282,13 @@ export default function PeriodSelectionScreen({
260282
onNext();
261283
} catch (error) {
262284
console.error('Failed to create goal:', error);
285+
setIsSubmitting(false);
263286
// Handle error appropriately (show toast, etc.)
264287
}
265288
}}
266289
disabled={!isNextEnabled}
267290
>
268-
다음
291+
{isSubmitting ? "처리 중..." : "다음"}
269292
</ButtonRound>
270293
</div>
271294
</div>

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"lint:fix": "next lint --fix",
1212
"lint:tsx": "eslint '**/*.{ts,tsx}' --max-warnings 0",
1313
"type-check": "tsc --noEmit",
14-
"gen": "node api/generator.mjs",
14+
"gen": "dotenv -e .env.dev -- node api/generator.mjs",
1515
"storybook": "storybook dev",
1616
"build-storybook": "storybook build",
1717
"build-storybook:shared": "STORYBOOK_SCOPE=shared storybook build -o storybook-static-shared",

0 commit comments

Comments
 (0)