-
Notifications
You must be signed in to change notification settings - Fork 2
Renew onboarding #57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Renew onboarding #57
Changes from all commits
23dbe48
698ed91
b6cb35d
55f4a75
2763cf3
cf2157a
4ec8eaf
2b3894e
d1ca615
0a0ea18
eda3e43
86acdfe
978b3aa
a16fd87
ee6afc7
70ebaa3
bcc1cd5
7d5250b
02b17e6
7041351
5bf678b
5104f2a
df656bb
a5446c8
fabde4e
68ce4ea
21316d4
272432d
2e05c03
583c613
d29eeff
7a7a68a
3136c68
8801660
5c7a59d
3dc406c
b2629d4
dfac342
f065d94
525b888
8bf61b6
f08aa2a
08b4560
ee7a20e
1eb042b
396aab4
9387475
ab5fa12
8ee477b
d294635
55a220a
f07a016
e1b6af9
891c5e5
c2f7bec
08de64e
3f3ad2d
3d64154
99ac5b6
1589563
6a2f808
25a404b
23bc5da
af4bcf6
7c97603
d494c02
d313a9c
059e897
15c7ec4
2e7f446
b2f3216
5f81d91
c4a0e97
b7c9b3a
074ebf4
bc22043
010a449
a5e0ba9
be8d56d
4b3ae63
3ac8db2
3544b0a
3df8115
f290846
6d9f881
e8f1572
5e429ea
96c1443
81f8a50
2880789
2b186c7
0fa30b6
48d36d9
7a294f4
f996952
fc1fc8c
0843420
e551646
8084437
4c51f90
7335ef3
398f6c5
d4527a1
80b7d35
43e0d7b
e5eebd3
742c292
fc3f8b0
6b0e02b
b05e3a5
efee88a
71b6498
b78646d
8543b55
e750173
7cd8527
6d665d9
f048da4
0a0bddf
8c66ce2
24a297c
d440072
2e172e3
0ffe198
b643f38
3677d39
3532d24
de52749
b9f5467
edd5910
dd74449
ef0e17d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| "use client"; | ||
| import { motion, Reorder } from "motion/react"; | ||
| import { useState, useEffect, useRef } from "react"; | ||
|
|
||
| import useModal from "@/hooks/useModal"; | ||
|
|
||
| import SubGoalEditItem from "@/components/details/SubGoalEditItem/SubGoalEditItem"; | ||
| import ModalAddingSubGoal from "@/components/shared/Modal/ModalAddingSubGoal/ModalAddingSubGoal"; | ||
| import ModalDeletingSubGoal from "@/components/shared/Modal/ModalDeletingSubGoal/ModalDeletingSubGoal"; | ||
| import ModalUpdatingSubGoal from "@/components/details/Modals/ModalUpdatingSubGoal/ModalUpdatingSubGoal"; | ||
|
|
||
| import PlusSvg from "@/components/shared/public/Add_Plus.svg"; | ||
| import useToast from "@/hooks/useToast"; | ||
| import useOnboardingStore from "@/stores/useOnboardingStore"; | ||
|
|
||
| const OnboardingSubGoalEdit = () => { | ||
| const { openModal, closeModal } = useModal(); | ||
| const { setToast } = useToast(); | ||
| const { subGoals, setSubGoals, addSubGoal } = useOnboardingStore(); | ||
| const hasInitialized = useRef(false); | ||
|
|
||
| // Add default "기본함" subGoal when component mounts if it doesn't exist | ||
| useEffect(() => { | ||
| if (hasInitialized.current) return; | ||
|
|
||
| const hasDefaultSubGoal = subGoals.some(subGoal => subGoal.title === "기본함"); | ||
|
|
||
| if (!hasDefaultSubGoal) { | ||
| addSubGoal("기본함"); | ||
| hasInitialized.current = true; | ||
| } else { | ||
| // Remove duplicate "기본함" subGoals, keep only the first one | ||
| const defaultSubGoals = subGoals.filter(subGoal => subGoal.title === "기본함"); | ||
| if (defaultSubGoals.length > 1) { | ||
| const filteredSubGoals = subGoals.filter(subGoal => subGoal.title !== "기본함"); | ||
| filteredSubGoals.unshift(defaultSubGoals[0]); // Keep the first "기본함" at the beginning | ||
| setSubGoals(filteredSubGoals); | ||
| } | ||
| hasInitialized.current = true; | ||
| } | ||
| }, [subGoals, addSubGoal, setSubGoals]); | ||
|
|
||
| return ( | ||
| <> | ||
| <main className="flex flex-col gap-2"> | ||
| <> | ||
| <div | ||
| data-type="type5" | ||
| className="w-full p-3 bg-white rounded-lg shadow-[0px_0px_4px_0px_rgba(0,0,0,0.10)] inline-flex flex-col justify-start items-center gap-2 overflow-hidden" | ||
| > | ||
| <div className="self-stretch h-8 inline-flex justify-start items-center gap-1"> | ||
| <div className="flex-1 justify-center text-label-strong text-base font-bold font-['SUIT_Variable'] leading-snug"> | ||
| 세부 목표를 추가해주세요. | ||
| </div> | ||
| <button | ||
| onClick={() => { | ||
| openModal( | ||
| <ModalAddingSubGoal | ||
| onClose={() => closeModal()} | ||
| onAddSubGoal={async (subGoal: string) => { | ||
| addSubGoal(subGoal); | ||
| closeModal(); | ||
| }} | ||
| />, | ||
| ); | ||
| }} | ||
| type="button" | ||
| className="w-8 h-8 p-1.5 bg-background-primary rounded-[999px] flex justify-center items-center gap-2" | ||
| > | ||
| <div className="w-5 h-5 relative overflow-hidden text-white"> | ||
| <PlusSvg width={20} height={20} /> | ||
| </div> | ||
| </button> | ||
| </div> | ||
| </div> | ||
| </> | ||
| <Reorder.Group | ||
| onReorder={(newOrderedSubGoals) => { | ||
| const orderUpdated = newOrderedSubGoals as typeof subGoals; | ||
| setSubGoals( | ||
| orderUpdated.map((subGoalInfo, idx) => ({ | ||
| ...subGoalInfo, | ||
| order: idx + 1, | ||
| })) | ||
| ); | ||
| }} | ||
| values={subGoals} | ||
| > | ||
| <section className="flex flex-col gap-2"> | ||
| {subGoals.map((currentSubGoalInfo, idx) => ( | ||
| <Reorder.Item | ||
| value={currentSubGoalInfo} | ||
| key={ | ||
| currentSubGoalInfo.id | ||
| ? `${currentSubGoalInfo.id}-${currentSubGoalInfo.title}` | ||
| : currentSubGoalInfo.tmpKey || currentSubGoalInfo.order | ||
| } | ||
| > | ||
| <SubGoalEditItem | ||
| subGoalTitle={currentSubGoalInfo.title} | ||
| onEdit={ | ||
| currentSubGoalInfo.title === "기본함" | ||
| ? undefined | ||
| : () => { | ||
| openModal( | ||
| <ModalUpdatingSubGoal | ||
| initSubGoal={currentSubGoalInfo.title} | ||
| onClose={closeModal} | ||
| onUpdateSubGoal={async (newSubGoalTitle) => { | ||
| const newSubGoals = subGoals.map((prevSubGoal) => { | ||
| if ( | ||
| prevSubGoal.tmpKey === currentSubGoalInfo.tmpKey || | ||
| prevSubGoal.id === currentSubGoalInfo.id | ||
| ) { | ||
| return { ...prevSubGoal, title: newSubGoalTitle }; | ||
| } | ||
| return prevSubGoal; | ||
| }); | ||
|
|
||
| setSubGoals(newSubGoals); | ||
| setToast("세부 목표 텍스트 변경이 완료되었습니다."); | ||
| }} | ||
| />, | ||
| ); | ||
| } | ||
| } | ||
| onDelete={ | ||
| currentSubGoalInfo.title === "기본함" | ||
| ? undefined | ||
| : () => { | ||
| openModal( | ||
| <ModalDeletingSubGoal | ||
| onClose={closeModal} | ||
| onDeletSubGoale={async () => { | ||
| const newSubGoals = subGoals.filter( | ||
| (prevSubGoal) => | ||
| prevSubGoal.tmpKey !== currentSubGoalInfo.tmpKey && | ||
| prevSubGoal.id !== currentSubGoalInfo.id | ||
| ); | ||
|
|
||
| setSubGoals(newSubGoals); | ||
| closeModal(); | ||
| setToast("세부 목표 삭제가 완료되었습니다."); | ||
| }} | ||
| />, | ||
| ); | ||
| } | ||
| } | ||
| /> | ||
| </Reorder.Item> | ||
| ))} | ||
| </section> | ||
| </Reorder.Group> | ||
| </main> | ||
| </> | ||
| ); | ||
| }; | ||
|
|
||
| export default OnboardingSubGoalEdit; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ import { ButtonRound } from "@/components/shared/ButtonRound/ButtonRound"; | |
| import { CupertinoPicker } from "@/components/shared/CupertinoPicker/CupertinoPicker"; | ||
| import api, { goalApi } from "@/api/service"; | ||
| import useOnboardingStore from "@/stores/useOnboardingStore"; | ||
| import useAuthStore from "@/stores/useAuthStore"; | ||
|
|
||
| interface PeriodSelectionScreenProps { | ||
| onNext: () => void; | ||
|
|
@@ -22,10 +23,13 @@ export default function PeriodSelectionScreen({ | |
| periodType, | ||
| monthCount, | ||
| targetDate, | ||
| subGoals, | ||
| setPeriodType, | ||
| setMonthCount, | ||
| setTargetDate, | ||
| } = useOnboardingStore(); | ||
|
|
||
| const { isGuest } = useAuthStore(); | ||
|
|
||
| const [currentMonth, setCurrentMonth] = useState(new Date().getMonth()); | ||
| const [currentYear, setCurrentYear] = useState(new Date().getFullYear()); | ||
|
|
@@ -272,17 +276,23 @@ export default function PeriodSelectionScreen({ | |
| })(); | ||
|
|
||
| try { | ||
| await goalApi.createGoal({ | ||
| title: goal, | ||
| isPeriodByMonth, | ||
| month: isPeriodByMonth ? monthCount : undefined, | ||
| dueDate, | ||
| subGoals: [], | ||
| }); | ||
| // Skip API call for guests | ||
| if (!isGuest) { | ||
| await goalApi.createGoal({ | ||
| title: goal, | ||
| isPeriodByMonth, | ||
| month: isPeriodByMonth ? monthCount : undefined, | ||
| dueDate, | ||
| subGoals: subGoals.map((subGoal, index) => ({ | ||
| title: subGoal.title, | ||
| order: index + 1, | ||
| })), | ||
| }); | ||
| } | ||
| onNext(); | ||
| } catch (error) { | ||
|
Comment on lines
278
to
293
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Possible double goal creation across onboarding steps (PeriodSelection + SubGoalSelection) This screen creates the goal (non-guest). SubGoalSelection also creates a goal when subGoals exist. This can lead to duplicate goals being created for the same onboarding flow. Recommend centralizing goal creation to a single step:
Let me know which approach you prefer; I can draft the refactor. 🤖 Prompt for AI Agents |
||
| console.error('Failed to create goal:', error); | ||
| setIsSubmitting(false); | ||
| setIsSubmitting(false); | ||
| // Handle error appropriately (show toast, etc.) | ||
| } | ||
| }} | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,101 @@ | ||||||||||||||||
| "use client"; | ||||||||||||||||
|
|
||||||||||||||||
| import { useState, useEffect } from "react"; | ||||||||||||||||
| import { AppBar } from "@/components/shared/AppBar/AppBar"; | ||||||||||||||||
| import { ButtonRound } from "@/components/shared/ButtonRound/ButtonRound"; | ||||||||||||||||
| import OnboardingSubGoalEdit from "./OnboardingSubGoalEdit"; | ||||||||||||||||
| import useOnboardingStore from "@/stores/useOnboardingStore"; | ||||||||||||||||
| import { goalApi } from "@/api/service"; | ||||||||||||||||
| import { GoalCreateRq } from "@/api/generated/motimo/Api"; | ||||||||||||||||
|
|
||||||||||||||||
|
Comment on lines
+8
to
+10
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add guest-mode bypass to align with PeriodSelection behavior PeriodSelection skips the API call for guests. This screen should mirror that to avoid network calls and side effects in guest mode. Apply this diff: import { goalApi } from "@/api/service";
import { GoalCreateRq } from "@/api/generated/motimo/Api";
+import useAuthStore from "@/stores/useAuthStore";📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
| interface SubGoalSelectionScreenProps { | ||||||||||||||||
| onNext: () => void; | ||||||||||||||||
| onBack: () => void; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| export default function SubGoalSelectionScreen({ | ||||||||||||||||
| onNext, | ||||||||||||||||
| onBack, | ||||||||||||||||
| }: SubGoalSelectionScreenProps) { | ||||||||||||||||
| const [isSubmitting, setIsSubmitting] = useState(false); | ||||||||||||||||
| const { goal, periodType, monthCount, targetDate, subGoals, setSubGoals } = | ||||||||||||||||
| useOnboardingStore(); | ||||||||||||||||
|
|
||||||||||||||||
|
Comment on lines
+20
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Wire in isGuest from the auth store Use the guest flag to conditionally skip creation. Apply this diff: const [isSubmitting, setIsSubmitting] = useState(false);
const { goal, periodType, monthCount, targetDate, subGoals, setSubGoals } =
useOnboardingStore();
+ const { isGuest } = useAuthStore();📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
| // Cleanup duplicate "기본함" subGoals when this screen loads | ||||||||||||||||
| useEffect(() => { | ||||||||||||||||
| const defaultSubGoals = subGoals.filter( | ||||||||||||||||
| (subGoal) => subGoal.title === "기본함", | ||||||||||||||||
| ); | ||||||||||||||||
| if (defaultSubGoals.length > 1) { | ||||||||||||||||
| const otherSubGoals = subGoals.filter( | ||||||||||||||||
| (subGoal) => subGoal.title !== "기본함", | ||||||||||||||||
| ); | ||||||||||||||||
| setSubGoals([defaultSubGoals[0], ...otherSubGoals]); | ||||||||||||||||
| } | ||||||||||||||||
| }, []); // eslint-disable-line react-hooks/exhaustive-deps | ||||||||||||||||
|
|
||||||||||||||||
| const handleSubmit = async () => { | ||||||||||||||||
| if (isSubmitting) return; | ||||||||||||||||
|
|
||||||||||||||||
| setIsSubmitting(true); | ||||||||||||||||
| try { | ||||||||||||||||
| if (subGoals.length > 0) { | ||||||||||||||||
| // Create goal with subgoals | ||||||||||||||||
| const goalData: GoalCreateRq = { | ||||||||||||||||
| title: goal, | ||||||||||||||||
| isPeriodByMonth: periodType === "months", | ||||||||||||||||
| ...(periodType === "months" | ||||||||||||||||
| ? { month: monthCount } | ||||||||||||||||
| : { dueDate: targetDate?.toISOString().split("T")[0] }), | ||||||||||||||||
| subGoals: subGoals.map((subGoal) => ({ | ||||||||||||||||
| title: subGoal.title, | ||||||||||||||||
| })), | ||||||||||||||||
| }; | ||||||||||||||||
|
|
||||||||||||||||
| await goalApi.createGoal(goalData); | ||||||||||||||||
| } | ||||||||||||||||
|
Comment on lines
+55
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Skip goal creation for guests Mirror the behavior from PeriodSelection. Apply this diff: - await goalApi.createGoal(goalData);
+ if (!isGuest) {
+ await goalApi.createGoal(goalData);
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
|
|
||||||||||||||||
| onNext(); | ||||||||||||||||
| } catch (error) { | ||||||||||||||||
| console.error("Failed to create goal:", error); | ||||||||||||||||
| setIsSubmitting(false); | ||||||||||||||||
| } | ||||||||||||||||
| }; | ||||||||||||||||
|
|
||||||||||||||||
| return ( | ||||||||||||||||
| <div className="min-h-screen bg-background-alternative flex flex-col"> | ||||||||||||||||
| <AppBar type="progress" progress={100} onBackClick={onBack} /> | ||||||||||||||||
| <div className="py-[8px]" /> | ||||||||||||||||
|
|
||||||||||||||||
| {/* Content */} | ||||||||||||||||
| <div className="flex-1 px-6"> | ||||||||||||||||
| {/* Title and Description */} | ||||||||||||||||
| <div className="mb-8"> | ||||||||||||||||
| <h1 className="text-2xl font-bold text-label-strong mb-2"> | ||||||||||||||||
| 세부 목표를 설정할 수 있어요. | ||||||||||||||||
| </h1> | ||||||||||||||||
| <p className="text-base font-normal text-label-alternative leading-[1.4]"> | ||||||||||||||||
| 세부 목표를 설정하지 않았다면,{"\n"}기본함에 자동으로 투두가 | ||||||||||||||||
| 들어가요. | ||||||||||||||||
| </p> | ||||||||||||||||
| </div> | ||||||||||||||||
|
|
||||||||||||||||
| {/* SubGoal Edit Component */} | ||||||||||||||||
| <div className="mb-8"> | ||||||||||||||||
| <OnboardingSubGoalEdit /> | ||||||||||||||||
| </div> | ||||||||||||||||
| </div> | ||||||||||||||||
|
|
||||||||||||||||
| {/* Next/Setup Later Button */} | ||||||||||||||||
| <div className="px-4 pb-14"> | ||||||||||||||||
| <ButtonRound onClick={handleSubmit} disabled={isSubmitting}> | ||||||||||||||||
| {isSubmitting | ||||||||||||||||
| ? "처리 중..." | ||||||||||||||||
| : subGoals.length > 1 | ||||||||||||||||
| ? "다음" | ||||||||||||||||
| : "나중에 설정하기"} | ||||||||||||||||
| </ButtonRound> | ||||||||||||||||
| </div> | ||||||||||||||||
| </div> | ||||||||||||||||
| ); | ||||||||||||||||
| } | ||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo in prop name
onDeletSubGoale.The modal callback prop has a typo: should be
onDeleteSubGoalinstead ofonDeletSubGoale.📝 Committable suggestion
🤖 Prompt for AI Agents