Skip to content

Commit 924028e

Browse files
authored
Merge pull request #44 from DDD-Community/feat/goal-modal
[feat] : 목표 생성 완료 모달 및 다이얼로그 컴포넌트 개선
2 parents 5c28933 + 628f908 commit 924028e

File tree

6 files changed

+89
-41
lines changed

6 files changed

+89
-41
lines changed

src/app/(auth)/signup/components/SignupDialogButton.tsx

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { useEffect, useState } from 'react';
44
import { useRouter } from 'next/navigation';
55
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/shared/components/dialog';
6+
import Button from '@/shared/components/navigation/Button';
67

78
interface SignupButtonProps {
89
isValid: boolean;
@@ -26,18 +27,12 @@ export const SignupDialogButton = ({ isValid, onClick, isSignupSuccess, isSubmit
2627

2728
return (
2829
<>
29-
<button
30-
type="button"
30+
<Button
31+
size={'ml'}
3132
onClick={onClick}
3233
disabled={!isValid || isSubmitting}
33-
className={`w-full py-3 rounded-lg font-medium transition-colors ${
34-
isValid && !isSubmitting
35-
? 'bg-white text-black hover:bg-gray-100'
36-
: 'bg-[#2C2C2E] text-gray-500 cursor-not-allowed'
37-
}`}
38-
>
39-
{isSubmitting ? '가입 중...' : '가입하기'}
40-
</button>
34+
text={isSubmitting ? '가입 중...' : '가입하기'}
35+
/>
4136
<Dialog open={showSuccessDialog} onOpenChange={setShowSuccessDialog}>
4237
<DialogContent className="sm:max-w-md">
4338
<DialogHeader>

src/app/(home)/home/create-goal/page.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,22 @@ export interface GoalFormData {
2121
asIs: string;
2222
toBe: string;
2323
};
24-
plans: { content: string }[];
24+
plans: {
25+
content: string;
26+
weekOfMonth: number;
27+
}[];
2528
}
2629

2730
const defaultValues: GoalFormData = {
2831
name: '',
2932
duration: { startDate: '', endDate: '' },
3033
beforeAfter: { asIs: '', toBe: '' },
31-
plans: [{ content: '' }, { content: '' }, { content: '' }, { content: '' }],
34+
plans: [
35+
{ content: '', weekOfMonth: 1 },
36+
{ content: '', weekOfMonth: 2 },
37+
{ content: '', weekOfMonth: 3 },
38+
{ content: '', weekOfMonth: 4 },
39+
],
3240
};
3341

3442
// 날짜를 YYYY-MM-DD 형식으로 변환하는 함수 (시간대 문제 해결)
@@ -74,8 +82,8 @@ export default function CreateGoalPage() {
7482
reValidateMode: 'onChange',
7583
defaultValues,
7684
});
77-
const { isLoading, isError, createGoal } = useFetchPostCreateGoal();
7885
const [completed, setCompleted] = useState(false);
86+
const { isLoading, isError, isSuccess, createGoal } = useFetchPostCreateGoal();
7987

8088
const startDate = watch('duration.startDate');
8189

@@ -254,6 +262,7 @@ export default function CreateGoalPage() {
254262
onComplete={handleSubmit(onSubmit)}
255263
isComplete={isValid && percent === 100 && !completed}
256264
isLoading={isLoading}
265+
isSuccess={isSuccess}
257266
isError={isError}
258267
/>
259268
</main>
Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import FlexBox from '@/shared/components/layout/FlexBox';
22
import Button from '@/shared/components/navigation/Button';
33
import { motion } from 'framer-motion';
4+
import { GoalDialogButton } from '@/feature/goal/components/GoalDialogButton';
45

56
interface ProgressBarProps {
67
doneTask?: number;
@@ -10,25 +11,19 @@ interface ProgressBarProps {
1011
onComplete: () => void;
1112
isComplete: boolean;
1213
isError?: boolean;
14+
isSuccess?: boolean;
1315
}
1416

1517
export const ConfirmGoalBottomBar = ({
16-
isLoading,
1718
doneTask,
1819
totalTask,
1920
percentage,
2021
onComplete,
22+
isLoading,
2123
isComplete,
2224
isError = false,
25+
isSuccess = false,
2326
}: ProgressBarProps) => {
24-
const isButtonEnabled = !isLoading && (isComplete || isError);
25-
26-
const getButtonText = () => {
27-
if (isLoading) return '로딩중';
28-
if (isError) return '다시 시도';
29-
return '목표 작성 완료';
30-
};
31-
3227
return (
3328
<div className="w-full flex items-center max-sm:flex-col max-sm:gap-[20px] max-sm:px-[20px] max-sm:pb-[48px] sm:gap-[60px] sm:px-[40px] sm:pb-[16px] pt-[20px] border-t border-line-normal">
3429
<div className="flex flex-col gap-2 items-start w-full">
@@ -46,9 +41,13 @@ export const ConfirmGoalBottomBar = ({
4641
/>
4742
</FlexBox>
4843
</div>
49-
<FlexBox className="max-sm:w-full sm:min-w-[140px]">
50-
<Button size="xl" text={getButtonText()} onClick={onComplete} disabled={!isButtonEnabled} />
51-
</FlexBox>
44+
<GoalDialogButton
45+
isLoading={isLoading}
46+
isComplete={isComplete}
47+
isSuccess={isSuccess}
48+
isError={isError}
49+
onClick={onComplete}
50+
/>
5251
</div>
5352
);
5453
};
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { useEffect, useState } from 'react';
2+
import Button from '@/shared/components/navigation/Button';
3+
import FlexBox from '@/shared/components/layout/FlexBox';
4+
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/shared/components/dialog';
5+
import { useRouter } from 'next/navigation';
6+
7+
interface GoalDialogButtonProps {
8+
isLoading: boolean;
9+
isComplete: boolean;
10+
isSuccess: boolean;
11+
isError: boolean;
12+
onClick: () => void;
13+
}
14+
15+
export const GoalDialogButton = ({ isLoading, isError, isComplete, isSuccess, onClick }: GoalDialogButtonProps) => {
16+
const router = useRouter();
17+
const [showSuccessDialog, setShowSuccessDialog] = useState(false);
18+
19+
const isButtonEnabled = !isLoading && (isComplete || isError);
20+
const getButtonText = () => {
21+
if (isLoading) return '로딩중';
22+
if (isError) return '다시 시도';
23+
return '목표 작성 완료';
24+
};
25+
const handleSuccessConfirm = () => {
26+
setShowSuccessDialog(false);
27+
router.push('/home');
28+
};
29+
30+
useEffect(() => {
31+
if (isSuccess) setShowSuccessDialog(true);
32+
}, [isSuccess]);
33+
34+
return (
35+
<>
36+
<FlexBox className="max-sm:w-full sm:min-w-[140px]">
37+
<Button size="xl" text={getButtonText()} onClick={onClick} disabled={!isButtonEnabled} />
38+
</FlexBox>
39+
<Dialog open={showSuccessDialog} onOpenChange={setShowSuccessDialog}>
40+
<DialogContent className="sm:max-w-md" showCloseButton={false}>
41+
<DialogHeader>
42+
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100 mb-4">
43+
<svg className="h-6 w-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
44+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
45+
</svg>
46+
</div>
47+
<DialogTitle className="text-center">축하한다냥 🐱</DialogTitle>
48+
<DialogDescription className="text-center">드디어 4주간 여정의 도착지가 정해졌다냥!</DialogDescription>
49+
</DialogHeader>
50+
<Button size={'ml'} text={'4주 여정 시작하기'} onClick={handleSuccessConfirm} />
51+
</DialogContent>
52+
</Dialog>
53+
</>
54+
);
55+
};

src/feature/goal/hooks/useFetchPostCreateGoal.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { useState, useEffect } from 'react';
2-
import { useRouter } from 'next/navigation';
1+
import { useState } from 'react';
32
import { postCreateGoal } from '../api/api';
43
import { GoalFormData } from '@/app/(home)/home/create-goal/page';
54
import { useToast } from '@/shared/components/feedBack/toast';
@@ -14,7 +13,6 @@ interface UseFetchPostCreateGoalReturn {
1413
}
1514

1615
export const useFetchPostCreateGoal = (): UseFetchPostCreateGoalReturn => {
17-
const router = useRouter();
1816
const { showToast } = useToast();
1917
const [isLoading, setIsLoading] = useState(false);
2018
const [isSuccess, setIsSuccess] = useState(false);
@@ -28,16 +26,8 @@ export const useFetchPostCreateGoal = (): UseFetchPostCreateGoalReturn => {
2826
setIsSuccess(false);
2927
setIsError(false);
3028
setError(null);
31-
3229
await postCreateGoal(data);
33-
3430
setIsSuccess(true);
35-
showToast('목표가 성공적으로 생성되었습니다!', 'success');
36-
37-
// 성공 토스트 메시지가 보인 후 home 페이지로 이동
38-
setTimeout(() => {
39-
router.push('/home');
40-
}, 500);
4131
} catch (err) {
4232
setIsError(true);
4333
const errorMessage = err instanceof Error ? err.message : '목표 생성에 실패했습니다.';

src/shared/components/dialog.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ function DialogOverlay({ className, ...props }: React.ComponentProps<typeof Dial
2727
<DialogPrimitive.Overlay
2828
data-slot="dialog-overlay"
2929
className={cn(
30-
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
30+
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
3131
className
3232
)}
3333
{...props}
@@ -49,7 +49,7 @@ function DialogContent({
4949
<DialogPrimitive.Content
5050
data-slot="dialog-content"
5151
className={cn(
52-
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
52+
'bg-elevated-normal data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg p-6 shadow-lg duration-200 sm:max-w-lg',
5353
className
5454
)}
5555
{...props}
@@ -58,7 +58,7 @@ function DialogContent({
5858
{showCloseButton && (
5959
<DialogPrimitive.Close
6060
data-slot="dialog-close"
61-
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
61+
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 text-label-normal"
6262
>
6363
<XIcon />
6464
<span className="sr-only">Close</span>
@@ -73,7 +73,7 @@ function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
7373
return (
7474
<div
7575
data-slot="dialog-header"
76-
className={cn('flex flex-col gap-2 text-center sm:text-left', className)}
76+
className={cn('flex flex-col gap-2 text-center sm:text-left text-label-normal', className)}
7777
{...props}
7878
/>
7979
);
@@ -93,7 +93,7 @@ function DialogTitle({ className, ...props }: React.ComponentProps<typeof Dialog
9393
return (
9494
<DialogPrimitive.Title
9595
data-slot="dialog-title"
96-
className={cn('text-lg leading-none font-semibold', className)}
96+
className={cn('text-lg leading-none font-semibold text-label-normal', className)}
9797
{...props}
9898
/>
9999
);

0 commit comments

Comments
 (0)