Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { cva } from 'class-variance-authority';
import TimePicker from '../timePicker/TimePicker';
import { AlarmsType } from '@constants/alarms';
import { useState } from 'react';
import { normalizeTime } from '@pages/onBoarding/utils/formatRemindTime';
interface AlarmBoxProps {
select: 1 | 2 | 3;
isDisabled: boolean;
Expand Down Expand Up @@ -57,7 +58,7 @@ const AlarmBox = ({ select, isDisabled, onClick }: AlarmBoxProps) => {
{select === 3 && isDisabled && (
<>
{AlarmsType[2].time && (
<p className="caption2-m text-font-gray-3">{AlarmsType[2].time}</p>
<p className="caption2-m text-font-gray-3">{normalizeTime(AlarmsType[2].time)}</p>
)}

{showPicker && (
Expand Down
28 changes: 25 additions & 3 deletions apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import AlarmStep from './step/AlarmStep';
import MacStep from './step/MacStep';
import FinalStep from './step/FinalStep';
import { cva } from 'class-variance-authority';
import { usePostSignUp } from '@shared/apis/queries';
const stepProgress = [{ progress: 30 }, { progress: 60 }, { progress: 100 }];

import { AlarmsType } from '@constants/alarms';
import { normalizeTime } from '@pages/onBoarding/utils/formatRemindTime';
const variants = {
slideIn: (direction: number) => ({
x: direction > 0 ? 200 : -200,
Expand Down Expand Up @@ -36,6 +38,8 @@ const MainCard = () => {
const [direction, setDirection] = useState(0);
const [alarmSelected, setAlarmSelected] = useState<1 | 2 | 3>(1);
const [isMac, setIsMac] = useState(false);
// api 구간
const {mutate:postSignData} = usePostSignUp();

Comment on lines +42 to 43
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

중복 제출 방지: 로딩 상태 처리 및 버튼 비활성화

useMutationisPending을 사용해 전송 중 중복 클릭을 막아주세요.

적용 제안(diff 1/2):

-  const {mutate:postSignData} = usePostSignUp();
+  const { mutate: postSignData, isPending } = usePostSignUp();

적용 제안(diff 2/2 — 파일 하단 버튼, 외부 범위 변경):

-        <Button
+        <Button
           variant="primary"
           size="medium"
-          isDisabled={step === 6}
+          isDisabled={step === 6 || isPending}
           className="ml-auto w-[4.8rem]"
           onClick={nextStep}
         >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const {mutate:postSignData} = usePostSignUp();
const { mutate: postSignData, isPending } = usePostSignUp();
...
<Button
variant="primary"
size="medium"
isDisabled={step === 6 || isPending}
className="ml-auto w-[4.8rem]"
onClick={nextStep}
>
🤖 Prompt for AI Agents
In apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx around lines
41-42, the mutation result from usePostSignUp is destructured without the
loading flag, allowing duplicate submissions; update the hook usage to also
destructure isPending (or equivalent loading flag) from useMutation, add a guard
in the submit handler that returns early if isPending, and pass isPending to the
submit button as disabled (and optionally show a spinner) so the button is
disabled while the request is in flight.

useEffect(() => {
const ua = navigator.userAgent.toLowerCase();
Expand All @@ -62,15 +66,33 @@ const MainCard = () => {
}
};

const [remindTime, setRemindTime] = useState('09:00');
const nextStep = () => {
console.log(step)
if (step === 3) {
// 이거 이후에 api 붙일 자리 표시임! console.log('선택된 알람:', AlarmsType[alarmSelected - 1].time);
// 이거 이후에 api 붙일 자리 표시임!

}
if (step < 5) {
setDirection(1);
setStep((prev) => prev + 1);
} else if (step === 5) {
window.location.href = '/';
const raw = AlarmsType[alarmSelected - 1].time;
setRemindTime(normalizeTime(raw));

postSignData({
"email": "tesdfdfsst@gmail.com",
"remindDefault": remindTime,
"fcmToken": "adlfdjlajlkadfsjlkfdsdfsdfsdfsdfsa"
},
Comment on lines +83 to +87
Copy link
Member

Choose a reason for hiding this comment

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

key에 string으로 저렇게 작성해도 잘 동작하나요??
추가로 email은 이후에 크롬에서 받아오는 로직이 추가되는거죠??

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

넵네! 첫 회원가입 로직이라서,
정보 중복에러뜨지 않게 계속 그때 그때 이메일이랑 토큰 수정해가며 요청 보내면 성공으로 잘 뜹니다!

{
onSuccess:()=>{
window.location.href = '/';
}
}
)


}
};

Expand Down
26 changes: 26 additions & 0 deletions apps/client/src/pages/onBoarding/utils/formatRemindTime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export function normalizeTime(input: string): string {
let hours = 0;
let minutes = 0;

const ampmMatch = input.match(/(AM|PM)\s?(\d{1,2}):(\d{1,2})/i);
if (ampmMatch) {
const [, ampm, h, m] = ampmMatch;
hours = parseInt(h, 10);
minutes = parseInt(m, 10);

if (ampm.toUpperCase() === "PM" && hours < 12) {
hours += 12;
}
if (ampm.toUpperCase() === "AM" && hours === 12) {
hours = 0; // 12 AM → 00시
}
} else {
const [h, m] = input.split(":");
hours = parseInt(h, 10);
minutes = parseInt(m, 10);
}

const hh = String(hours).padStart(2, "0");
const mm = String(minutes).padStart(2, "0");
return `${hh}:${mm}`;
}
Comment on lines +1 to +26
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

한글(오전/오후) 포맷 미지원으로 NaN:NaN 발생 가능

현재 AlarmsType 기본값(예: 오전 9시, 오후 8시)을 normalizeTime에 전달하면 정규식이 매칭되지 않아 parseIntNaN을 반환, 결과가 "NaN:NaN"이 됩니다. 한글(오전/오후)과 ~시(~분) 포맷을 지원하고, 입력 검증/기본값을 추가해주세요.

아래처럼 다국어 포맷을 안전하게 처리하는 방향을 제안합니다.

-export function normalizeTime(input: string): string {
-  let hours = 0;
-  let minutes = 0;
-
-  const ampmMatch = input.match(/(AM|PM)\s?(\d{1,2}):(\d{1,2})/i);
-  if (ampmMatch) {
-    const [, ampm, h, m] = ampmMatch;
-    hours = parseInt(h, 10);
-    minutes = parseInt(m, 10);
-
-    if (ampm.toUpperCase() === "PM" && hours < 12) {
-      hours += 12;
-    }
-    if (ampm.toUpperCase() === "AM" && hours === 12) {
-      hours = 0; // 12 AM → 00시
-    }
-  } else {
-    const [h, m] = input.split(":");
-    hours = parseInt(h, 10);
-    minutes = parseInt(m, 10);
-  }
-
-  const hh = String(hours).padStart(2, "0");
-  const mm = String(minutes).padStart(2, "0");
-  return `${hh}:${mm}`;
-}
+export function normalizeTime(input: string): string {
+  const fallback = '09:00';
+  if (!input || typeof input !== 'string') return fallback;
+  const s = input.trim();
+
+  // 1) AM/PM 앞: "AM 9:05", "PM 12:00"
+  const ampmFront = s.match(/^(AM|PM)\s*(\d{1,2})(?::(\d{1,2}))?$/i);
+  // 2) AM/PM 뒤: "9:05 AM", "12 PM"
+  const ampmBack = s.match(/^(\d{1,2})(?::(\d{1,2}))?\s*(AM|PM)$/i);
+  // 3) 한글: "오전 9시", "오후 8시", "오전 9시 5분"
+  const kor = s.match(/^(오전|오후)\s*(\d{1,2})\s*시(?:\s*(\d{1,2})\s*분)?$/);
+  // 4) 24시간: "09:00", "9:5"
+  const h24 = s.match(/^(\d{1,2})(?::(\d{1,2}))$/);
+
+  let hours: number | null = null;
+  let minutes: number | null = null;
+  let meridiem: 'AM' | 'PM' | null = null;
+
+  if (ampmFront) {
+    const [, ampm, h, m] = ampmFront;
+    hours = parseInt(h!, 10);
+    minutes = m ? parseInt(m, 10) : 0;
+    meridiem = ampm.toUpperCase() as 'AM' | 'PM';
+  } else if (ampmBack) {
+    const [, h, m, ampm] = ampmBack;
+    hours = parseInt(h!, 10);
+    minutes = m ? parseInt(m, 10) : 0;
+    meridiem = ampm.toUpperCase() as 'AM' | 'PM';
+  } else if (kor) {
+    const [, ap, h, m] = kor;
+    hours = parseInt(h!, 10);
+    minutes = m ? parseInt(m, 10) : 0;
+    meridiem = ap === '오후' ? 'PM' : 'AM';
+  } else if (h24) {
+    const [, h, m] = h24;
+    hours = parseInt(h!, 10);
+    minutes = parseInt(m!, 10);
+  }
+
+  if (hours === null || minutes === null || Number.isNaN(hours) || Number.isNaN(minutes)) {
+    return fallback;
+  }
+
+  if (meridiem) {
+    if (meridiem === 'PM' && hours < 12) hours += 12;
+    if (meridiem === 'AM' && hours === 12) hours = 0;
+  }
+
+  if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return fallback;
+
+  const hh = String(hours).padStart(2, '0');
+  const mm = String(minutes).padStart(2, '0');
+  return `${hh}:${mm}`;
+}

추가로 간단한 유닛 테스트(예: '오전 9시'→'09:00', '오후 8시'→'20:00', 'AM 9:05'→'09:05', 빈 문자열→'09:00')도 부탁드립니다.

원하시면 테스트 템플릿을 바로 드리겠습니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function normalizeTime(input: string): string {
let hours = 0;
let minutes = 0;
const ampmMatch = input.match(/(AM|PM)\s?(\d{1,2}):(\d{1,2})/i);
if (ampmMatch) {
const [, ampm, h, m] = ampmMatch;
hours = parseInt(h, 10);
minutes = parseInt(m, 10);
if (ampm.toUpperCase() === "PM" && hours < 12) {
hours += 12;
}
if (ampm.toUpperCase() === "AM" && hours === 12) {
hours = 0; // 12 AM → 00시
}
} else {
const [h, m] = input.split(":");
hours = parseInt(h, 10);
minutes = parseInt(m, 10);
}
const hh = String(hours).padStart(2, "0");
const mm = String(minutes).padStart(2, "0");
return `${hh}:${mm}`;
}
export function normalizeTime(input: string): string {
const fallback = '09:00';
if (!input || typeof input !== 'string') return fallback;
const s = input.trim();
// 1) AM/PM front: "AM 9:05", "PM 12:00"
const ampmFront = s.match(/^(AM|PM)\s*(\d{1,2})(?::(\d{1,2}))?$/i);
// 2) AM/PM back: "9:05 AM", "12 PM"
const ampmBack = s.match(/^(\d{1,2})(?::(\d{1,2}))?\s*(AM|PM)$/i);
// 3) Korean: "오전 9시", "오후 8시", "오전 9시 5분"
const kor = s.match(/^(|)\s*(\d{1,2})\s*(?:\s*(\d{1,2})\s*)?$/);
// 4) 24-hour: "09:00", "9:5"
const h24 = s.match(/^(\d{1,2})(?::(\d{1,2}))$/);
let hours: number | null = null;
let minutes: number | null = null;
let meridiem: 'AM' | 'PM' | null = null;
if (ampmFront) {
const [, ampm, h, m] = ampmFront;
hours = parseInt(h!, 10);
minutes = m ? parseInt(m, 10) : 0;
meridiem = ampm.toUpperCase() as 'AM' | 'PM';
} else if (ampmBack) {
const [, h, m, ampm] = ampmBack;
hours = parseInt(h!, 10);
minutes = m ? parseInt(m, 10) : 0;
meridiem = ampm.toUpperCase() as 'AM' | 'PM';
} else if (kor) {
const [, ap, h, m] = kor;
hours = parseInt(h!, 10);
minutes = m ? parseInt(m, 10) : 0;
meridiem = ap === '오후' ? 'PM' : 'AM';
} else if (h24) {
const [, h, m] = h24;
hours = parseInt(h!, 10);
minutes = parseInt(m!, 10);
}
if (hours === null || minutes === null || Number.isNaN(hours) || Number.isNaN(minutes)) {
return fallback;
}
if (meridiem) {
if (meridiem === 'PM' && hours < 12) hours += 12;
if (meridiem === 'AM' && hours === 12) hours = 0;
}
if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return fallback;
const hh = String(hours).padStart(2, '0');
const mm = String(minutes).padStart(2, '0');
return `${hh}:${mm}`;
}
🤖 Prompt for AI Agents
In apps/client/src/pages/onBoarding/utils/formatRemindTime.ts around lines 1 to
26, normalizeTime currently only handles English AM/PM with HH:MM and will
return "NaN:NaN" for Korean formats like "오전 9시" or other malformed inputs;
update the function to (1) accept Korean AM/PM indicators (오전/오후) and both "시"
and optional "분" forms (e.g., "오전 9시", "오후 8시 30분"), (2) accept plain "HH:MM"
and AM/PM variants case-insensitively, (3) validate parsed numbers and
clamp/fallback to a sensible default time when parsing fails (use the component
default such as "09:00" if input is empty/invalid), and (4) ensure 24-hour
conversion logic is correct for 12 AM/PM edge cases; also add unit tests
verifying translations like '오전 9시'→'09:00', '오후 8시'→'20:00', 'AM 9:05'→'09:05',
and ''→'09:00'.

11 changes: 11 additions & 0 deletions apps/client/src/shared/apis/axios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,14 @@ export const getAcorns = async () => {
});
return data.data;
};

export interface postSignUpRequest {
email: string,
remindDefault: string,
fcmToken: string
}

export const postSignUp = async (responsedata: postSignUpRequest) => {
const {data} = await apiRequest.post('/api/v1/auth/signup', responsedata);
return data;
};
20 changes: 19 additions & 1 deletion apps/client/src/shared/apis/queries.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useMutation, useQuery, UseQueryResult } from '@tanstack/react-query';
import { getDashboardCategories, postCategory } from '@shared/apis/axios';
import { getDashboardCategories, postCategory, postSignUp, postSignUpRequest } from '@shared/apis/axios';
import { AxiosError } from 'axios';
import { DashboardCategoriesResponse, AcornsResponse } from '@shared/types/api';
import { getAcorns } from './axios';
Expand All @@ -26,3 +26,21 @@ export const useGetArcons = (): UseQueryResult<AcornsResponse, AxiosError> => {
queryFn: () => getAcorns(),
});
};

export const usePostSignUp = () => {
return useMutation({
mutationFn: (data: postSignUpRequest) => postSignUp(data),
onSuccess: (data) => {
Comment on lines +30 to +33
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

useMutation 제네릭 지정으로 응답 스키마 고정 및 안전성 확보

현재 onSuccessdata는 암시적으로 any로 추론되어 data?.data?.token || data?.token 같은 방어적 접근이 필요합니다. 응답 타입을 명시해 런타임 분기와 널러블 체이닝을 제거하세요. 동시에 에러 타입도 AxiosError로 고정합니다.

추가 타입(파일 상단 임의 위치):

type SignUpResponse = { token: string };

선택 라인 변경:

-export const usePostSignUp = () => {
-  return useMutation({
-    mutationFn: (data: postSignUpRequest) => postSignUp(data),
-    onSuccess: (data) => {
+export const usePostSignUp = () => {
+  return useMutation<SignUpResponse, AxiosError, postSignUpRequest>({
+    mutationFn: (payload) => postSignUp(payload),
+    onSuccess: (resp) => {
🤖 Prompt for AI Agents
In apps/client/src/shared/apis/queries.ts around lines 30 to 33, the useMutation
call lacks generics so onSuccess receives an any-shaped response and the code
defensively checks data?.data?.token; declare a SignUpResponse type (e.g. {
token: string }) near the top of the file, import AxiosError from axios, and
call useMutation with generics so it is useMutation<SignUpResponse, AxiosError,
postSignUpRequest>; then update mutationFn and onSuccess to accept strongly
typed values so you can directly read response.token (no nullable chaining) and
remove runtime branching.

const newToken = data?.data?.token || data?.token;

if (newToken) {
localStorage.setItem("token", newToken);
}

console.log("회원가입 성공:", data);
Copy link
Member

Choose a reason for hiding this comment

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

불필요한 콘솔은 이후에 제거!

},
onError: (error) => {
console.error("회원가입 실패:", error);
},
});
};
Loading