Conversation
Walkthrough온보딩 MainCard에서 React Query Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User as 사용자
participant MC as MainCard
participant RQ as usePostSignUp (React Query)
participant API as /api/v1/auth/signup
User->>MC: 온보딩 진행, 최종 단계 도달 (step === 5)
MC->>RQ: mutate({ email, remindDefault, fcmToken })
RQ->>API: POST /api/v1/auth/signup { email, remindDefault, fcmToken }
API-->>RQ: 200 OK / Error
alt 성공
RQ-->>MC: onSuccess (토큰 응답 가능)
MC->>MC: window.location.href = "/"
else 실패
RQ-->>MC: onError (콘솔 로그)
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests
📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
✅ Storybook chromatic 배포 확인: |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (1)
72-86: 최종 단계에서 한 번 더 클릭해야 호출되는 오프바이원 이슈 + 비 macOS에서 빈 화면 가능
- 현재 로직은
step === 5에서만 가입 API를 호출합니다. 비 macOS 경로에서는 마지막 화면이step === 4이며,step === 5에서는renderStep()이null을 반환해 빈 화면에서 한 번 더 클릭해야 호출됩니다.- macOS 경로도 최종 CTA에서 한 번 더 클릭해야 호출됩니다.
마지막 가시 단계에서 즉시 호출되도록 분기 조건을 조정하세요.
적용 제안(diff):
- } else if (step === 5) { + } else if ((isMac && step === 5) || (!isMac && step === 4)) { postSignData({ "email": "tesdfdfsst@gmail.com", "remindDefault": "08:00", "fcmToken": "adlfdjlajlkadfsjlkfdsdfsdfsdfsdfsa" }, { onSuccess:()=>{ window.location.href = '/'; } } ) - - }선택적으로 더 명시적인 구현을 원하시면,
lastVisibleStep을 도입해 가독성을 높일 수 있습니다(선택 적용):const lastVisibleStep = isMac ? 5 : 4; const nextStep = () => { if (step >= lastVisibleStep) { postSignData(/* ... */); return; } setDirection(1); setStep((prev) => prev + 1); };
🧹 Nitpick comments (5)
apps/client/src/shared/apis/axios.ts (1)
24-28: 타입 네이밍 컨벤션(PascalCase) 및 세미콜론 일관화Interface 이름을 PascalCase로 맞추고(다른 타입들과 일관성), 필드 끝에 세미콜론을 붙여 코딩 컨벤션을 통일합시다.
적용 제안(diff):
-export interface postSignUpRequest { - email: string, - remindDefault: string, - fcmToken: string -} +export interface PostSignUpRequest { + email: string; + remindDefault: string; + fcmToken: string; +}추가로 이 변경을 적용하면, 본 파일과 사용처의 제네릭/파라미터 타입 참조도
PostSignUpRequest로 함께 변경이 필요합니다.apps/client/src/shared/apis/queries.ts (2)
2-2: 타입 전용 import로 트리셰이킹/번들 최적화타입은
import type으로 분리하는 것이 좋습니다. (axios.ts에서 타입명을PostSignUpRequest로 변경하지 않는다면 기존 이름을 그대로 사용해도 됩니다.)적용 제안(diff, 타입명 변경 전제 없음):
-import { getDashboardCategories, postCategory, postSignUp, postSignUpRequest } from '@shared/apis/axios'; +import { getDashboardCategories, postCategory, postSignUp } from '@shared/apis/axios'; +import type { postSignUpRequest } from '@shared/apis/axios';
30-41: 뮤테이션 제네릭/키 지정으로 타입 안정성·디버깅 향상React Query에 제네릭과
mutationKey를 지정하면 DX가 좋아집니다. 에러 타입도AxiosError로 명시하세요.적용 제안(diff):
-export const usePostSignUp = () =>{ - return useMutation({ - mutationFn: (data: postSignUpRequest) => postSignUp(data), +export const usePostSignUp = () => { + return useMutation<unknown, AxiosError, postSignUpRequest>({ + mutationKey: ['auth', 'signup'], + mutationFn: (data) => postSignUp(data), onSuccess: (data) => { console.log("회원가입 성공:", data); }, onError: (error) => { console.error("회원가입 실패:", error); }, }); -} +};추가 제안(선택): 성공/실패에 대한 사이드이펙트는 호출자(MainCard)에서 처리하고, 훅은 순수하게 네트워크만 담당하도록 유지하는 것을 권장합니다.
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (2)
76-86: 하드코딩된 더미 페이로드 제거 필요PR 설명에 “임의의 로우데이터”라고 명시되어 있으나, 머지 전에는 실제 수집된 이메일/리마인드 시간/FCM 토큰을 사용하도록 교체해야 합니다. 값이 없을 때를 대비한 유효성 검증도 추가해 주세요.
예시:
if (!email || !remindDefault) { // 사용자 안내 및 조기 반환 return; } postSignData({ email, remindDefault, fcmToken }, { onSuccess: /* ... */ });
83-84: SPA 내비게이션 권장가능하면 전체 리로드(
window.location.href) 대신 라우터의navigate/push를 사용해 UX를 유지하는 것을 권장합니다. 확장 환경 제약으로 전체 리로드가 필요하다면 현 방식 유지 OK.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx(3 hunks)apps/client/src/shared/apis/axios.ts(1 hunks)apps/client/src/shared/apis/queries.ts(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (1)
apps/client/src/shared/apis/queries.ts (1)
usePostSignUp(30-41)
apps/client/src/shared/apis/queries.ts (1)
apps/client/src/shared/apis/axios.ts (2)
postSignUpRequest(24-28)postSignUp(30-33)
| const {mutate:postSignData} = usePostSignUp(); | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
중복 제출 방지: 로딩 상태 처리 및 버튼 비활성화
useMutation의 isPending을 사용해 전송 중 중복 클릭을 막아주세요.
적용 제안(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.
| 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.
apps/client/src/shared/apis/axios.ts
Outdated
| export const postSignUp = async (data: postSignUpRequest) => { | ||
| const response = await apiRequest.post('/api/v1/auth/signup', {data}); | ||
| return response.data; | ||
| }; No newline at end of file |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
응답 스키마(data vs data.data) 일관성 확인 필요
본 함수는 response.data를 반환하지만, 같은 파일의 다른 API(getDashboardCategories 등)는 data.data를 반환합니다. 호출부에서 혼선을 막기 위해 통일이 필요합니다. 실제 백엔드 응답 래핑 형식을 확인해 주세요.
다음 스크립트로 현재 반환 형태 분포를 확인할 수 있습니다:
🏁 Script executed:
#!/bin/bash
# axios API 반환 형태 점검
rg -nC1 -g 'apps/client/src/shared/apis/axios.ts' -e 'return .*data\.data;' -e 'return .*response\.data;'Length of output: 587
응답 스키마 불일치 — 통일 필요 (apps/client/src/shared/apis/axios.ts)
getDashboardCategories 등은 return data.data;(줄 6, 21)로 반환되는 반면 postSignUp은 return response.data;(줄 32)로 반환됩니다. 백엔드 응답 래핑 규격에 맞춰 모든 API의 반환 형태를 통일하세요 — (예: 백엔드가 { data: ... }로 래핑한다면 postSignUp도 data.data로 변경).
🤖 Prompt for AI Agents
In apps/client/src/shared/apis/axios.ts around lines 30 to 33, postSignUp
currently returns response.data which is inconsistent with other APIs that
return data.data; change postSignUp to unwrap the backend wrapper and return
response.data.data (or destructure to const { data } = response.data and return
data) so its return shape matches the backend's { data: ... } convention used by
getDashboardCategories and others; also verify the request body isn’t
double-wrapped when calling apiRequest.post (pass the payload directly if other
callers do so).
회원가입 요청 바디가 {data}로 래핑되어 전송됨 — 스펙 불일치 가능성 큼
동일 파일의 postCategory는 평평한 JSON을 보내는데, 여기만 { data: ... }로 감싸 전송합니다. 백엔드가 중첩 data를 기대하지 않으면 400/422가 납니다. 평평한 바디로 보내는 게 안전합니다.
적용 제안(diff):
-export const postSignUp = async (data: postSignUpRequest) => {
- const response = await apiRequest.post('/api/v1/auth/signup', {data});
+export const postSignUp = async (payload: postSignUpRequest) => {
+ const response = await apiRequest.post('/api/v1/auth/signup', payload);
return response.data;
};📝 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.
| export const postSignUp = async (data: postSignUpRequest) => { | |
| const response = await apiRequest.post('/api/v1/auth/signup', {data}); | |
| return response.data; | |
| }; | |
| export const postSignUp = async (payload: postSignUpRequest) => { | |
| const response = await apiRequest.post('/api/v1/auth/signup', payload); | |
| return response.data; | |
| }; |
🤖 Prompt for AI Agents
In apps/client/src/shared/apis/axios.ts around lines 30 to 33, the signup
request body is being sent wrapped as {data} which creates a nested payload that
likely violates the backend spec; change the POST call to send the plain data
object (i.e., pass data directly as the second argument to apiRequest.post) to
match postCategory’s flat JSON shape and adjust the types if needed so the
request body remains properly typed.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/client/src/pages/onBoarding/components/funnel/AlarmBox.tsx (1)
27-32: 전역 상수(AlarmsType) 직접 변이 지양 + 취소 시 모달 닫힘 누락
AlarmsType[2].time직접 대입은 전역 상태 변이라 예기치 않은 재렌더/동시성 이슈를 유발합니다. 지역 상태 또는 상위(MainCard)로 상태를 올리는 방식을 권장합니다.onCancel에서setShowPicker(false)가 없어 피커가 닫히지 않습니다.아래처럼 최소 수정으로 취소 시 닫히도록 반영해주세요. 전역 변이 제거는 추후 단계적으로 반영 권장합니다.
- onCancel={() => { - AlarmsType[2].time = ''; - }} + onCancel={() => { + AlarmsType[2].time = ''; + setShowPicker(false); + }}Also applies to: 67-71
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (1)
50-66: 비맥 사용자 경로에서 step 5가 빈 화면이 되어 1회 추가 클릭 필요
renderStep에서 비맥(isMac=false) 시 step 4는FinalStep, step 5는null이라, 사용자에게 빈 화면이 보이고 버튼을 한 번 더 눌러야 API가 호출됩니다. “마지막 화면에서 다음 클릭 시 즉시 가입 API 호출”로 통일해주세요.아래처럼 “플로우의 마지막 스텝”을 동적으로 계산해 처리하면 UX가 매끄럽습니다.
- const nextStep = () => { + const nextStep = () => { console.log(step) - if (step === 3) { + if (step === 3) { // 이거 이후에 api 붙일 자리 표시임! const raw = AlarmsType[alarmSelected - 1].time; setRemindTime(normalizeTime(raw)) } - if (step < 5) { - setDirection(1); - setStep((prev) => prev + 1); - } else if (step === 5) { + const lastStep = isMac ? 5 : 4; + if (step < lastStep) { + setDirection(1); + setStep((prev) => prev + 1); + return; + } + // 마지막 스텝에서 가입 요청 postSignData({ "email": "tesdfdfsst@gmail.com", "remindDefault": remindTime, "fcmToken": "adlfdjlajlkadfsjlkfdsdfsdfsdfsdfsa" }, { onSuccess:()=>{ window.location.href = '/'; } } ) - - - } + }
♻️ Duplicate comments (1)
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (1)
41-43: 중복 제출 방지: 로딩 상태로 버튼 비활성화 및 가드 추가이전 코멘트와 동일 맥락입니다.
isPending을 사용해 중복 클릭을 막아주세요. 제출 핸들러 초입에서도if (isPending) return;가드를 추천합니다.- const {mutate:postSignData} = usePostSignUp(); + const { mutate: postSignData, isPending } = usePostSignUp();- <Button + <Button variant="primary" size="medium" - isDisabled={step === 6} + isDisabled={step === 6 || isPending} className="ml-auto w-[4.8rem]" onClick={nextStep} >Also applies to: 145-152
🧹 Nitpick comments (1)
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (1)
71-71: 디버그 로그 정리
console.log(step)는 제거 또는 개발 환경에서만 출력되도록 조정해주세요.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
apps/client/src/pages/onBoarding/components/funnel/AlarmBox.tsx(2 hunks)apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx(3 hunks)apps/client/src/pages/onBoarding/utils/formatRemindTime.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
apps/client/src/pages/onBoarding/components/funnel/AlarmBox.tsx (2)
apps/client/src/pages/onBoarding/utils/formatRemindTime.ts (1)
normalizeTime(1-26)apps/client/src/constants/alarms.ts (1)
AlarmsType(11-15)
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (3)
apps/client/src/shared/apis/queries.ts (1)
usePostSignUp(30-41)apps/client/src/constants/alarms.ts (1)
AlarmsType(11-15)apps/client/src/pages/onBoarding/utils/formatRemindTime.ts (1)
normalizeTime(1-26)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: storybook
🔇 Additional comments (2)
apps/client/src/pages/onBoarding/components/funnel/AlarmBox.tsx (1)
61-61: 사용자 지정 시간 표시 시 포맷 일관성 확보된 점 좋습니다
normalizeTime으로 화면 표시는 안정적입니다. (API 전송과 동일 포맷 유지)apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (1)
82-92: postSignUp 페이로드 불일치 지적은 부정확 — navigate 사용 권장
- apps/client/src/shared/apis/axios.ts의 postSignUp(postSignUpRequest)는 전달된 객체를 내부에서 apiRequest.post('/api/v1/auth/signup', {data})로 { data: ... } 형태로 전송하므로 MainCard의 postSignData({ ... }) 호출은 시그니처/타입과 일치합니다.
- 선택적 리팩터: 성공 후 전체 리로드(window.location.href) 대신 react-router-dom의 useNavigate로 SPA 내 라우팅 전환 권장 (예: const navigate = useNavigate(); onSuccess: () => navigate('/');). 적용 위치: apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (mutate 사용부).
Likely an incorrect or invalid review comment.
| const [remindTime, setRemindTime] = useState('09:00'); | ||
| const nextStep = () => { | ||
| console.log(step) | ||
| if (step === 3) { | ||
| // 이거 이후에 api 붙일 자리 표시임! console.log('선택된 알람:', AlarmsType[alarmSelected - 1].time); | ||
| // 이거 이후에 api 붙일 자리 표시임! | ||
| const raw = AlarmsType[alarmSelected - 1].time; | ||
| setRemindTime(normalizeTime(raw)) | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
기본 알람(1,2) 선택 시 normalizeTime('오전 9시') → NaN:NaN
현재 상수의 한글 포맷을 그대로 넘기면 normalizeTime이 실패합니다. 위 유틸 수정이 반영되기 전까지는 여기서도 방어 코드를 두거나, 1/2 선택 시 하드코딩 맵핑(오전 9시→09:00, 오후 8시→20:00)으로 임시 대응해주세요.
수정 후 1/2/3 각 선택 케이스에서 remindTime이 'HH:MM'으로 정상 세팅되는지 확인 부탁드립니다.
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== rg: normalizeTime ==="
rg -n --hidden -S "normalizeTime" -g '!node_modules' -C3 || true
echo "=== rg: AlarmsType ==="
rg -n --hidden -S "AlarmsType" -g '!node_modules' -C3 || true
if [ -f "apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx" ]; then
echo "=== cat: apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (1-320) ==="
sed -n '1,320p' apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx || true
else
echo "MainCard.tsx not found at expected path"
fi
echo "=== search: Korean time literals (오전|오후) ==="
rg -n --hidden -S "오전|오후" -g '!node_modules' -C2 || true
echo "=== search: '09:00' or '20:00' literals ==="
rg -n --hidden -S "09:00|20:00" -g '!node_modules' -C2 || true
echo "=== search: AlarmsType array/object definitions ==="
rg -n --hidden -S "const\\s+\\w*Alarms\\w*\\s*=|export\\s+const\\s+\\w*Alarms\\w*\\s*=" -g '!node_modules' -C4 || true
echo "=== search: normalizeTime implementation ==="
rg -n --hidden -S "function\\s+normalizeTime|normalizeTime\\s*=|normalizeTime\\s*:\\s*\\(" -g '!node_modules' -C4 || true
echo "=== done ==="Length of output: 5565
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "PWD: $(pwd)"
echo "=== list top-level ==="
ls -la | sed -n '1,200p'
echo
echo "=== check common paths for formatRemindTime / alarms ==="
paths=(
"apps/client/src/pages/onBoarding/utils/formatRemindTime.ts"
"apps/client/src/pages/onBoarding/utils/formatRemindTime.tsx"
"apps/client/src/pages/onBoarding/utils/formatRemindTime/index.ts"
"apps/client/src/pages/onBoarding/utils/formatRemindTime/index.tsx"
"apps/client/src/constants/alarms.ts"
"apps/client/src/constants/alarms/index.ts"
"apps/client/src/constants/alarms.tsx"
"src/constants/alarms.ts"
"src/pages/onBoarding/utils/formatRemindTime.ts"
"apps/client/src/constants/alarms/index.ts"
)
for p in "${paths[@]}"; do
if [ -f "$p" ]; then
echo "FOUND: $p"
sed -n '1,240p' "$p" || true
echo "----"
else
echo "MISSING: $p"
fi
done
echo
echo "=== find files with relevant names (case-insensitive) ==="
find . -type f \( -iname '*formatremindtime*' -o -iname '*formatremind*' -o -iname '*alarms*' \) -print | sed -n '1,200p' || true
echo
echo "=== grep for symbols (exclude node_modules) ==="
grep -RIn --exclude-dir=node_modules --include=\*.{ts,tsx,js,jsx} "normalizeTime" . || true
grep -RIn --exclude-dir=node_modules --include=\*.{ts,tsx,js,jsx} "formatRemindTime" . || true
grep -RIn --exclude-dir=node_modules --include=\*.{ts,tsx,js,jsx} "AlarmsType" . || true
echo
echo "=== list utils and constants dirs if present ==="
[ -d "apps/client/src/pages/onBoarding/utils" ] && ls -la "apps/client/src/pages/onBoarding/utils" || true
[ -d "apps/client/src/constants" ] && ls -la "apps/client/src/constants" || true
[ -d "src/pages/onBoarding/utils" ] && ls -la "src/pages/onBoarding/utils" || true
[ -d "src/constants" ] && ls -la "src/constants" || trueLength of output: 6206
기본 알람(1/2) 선택 시 시간 포맷 방어 필요 — '오전 9시' 입력으로 normalizeTime이 NaN:NaN 반환
- 원인: apps/client/src/pages/onBoarding/utils/formatRemindTime.ts는 "AM|PM HH:MM" 또는 "HH:MM"만 파싱하며, apps/client/src/constants/alarms.ts는
'오전 9시'/'오후 8시'문자열을 사용함. - 조치(택1): 유틸에
'오전'|'오후' X시형태 파싱을 추가하거나 MainCard(nextStep)에서 alarmSelected 1/2에 대해 하드코딩 매핑('오전 9시'→'09:00','오후 8시'→'20:00') 또는 입력 유효성 검사(유효하지 않으면 기본값 사용) 적용. - 수정 위치: apps/client/src/pages/onBoarding/utils/formatRemindTime.ts, apps/client/src/constants/alarms.ts, apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (nextStep).
- 검증: 변경 후 1/2/3 선택 케이스에서 remindTime이 모두 'HH:MM' 포맷으로 정상 세팅되는지 확인.
🤖 Prompt for AI Agents
In apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx around lines
69-76, nextStep calls normalizeTime on alarm strings like '오전 9시' which
formatRemindTime doesn't parse, causing NaN:NaN; fix by handling alarmSelected
cases 1 and 2 explicitly: map the known Korean strings to '09:00' and '20:00'
(or the correct HH:MM values) before calling normalizeTime, and for other cases
call normalizeTime with a validation/fallback (if normalizeTime returns invalid,
set remindTime to a safe default like '09:00'); implement this
mapping/validation in nextStep so remindTime is always set to a valid 'HH:MM'
string.
| 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}`; | ||
| } No newline at end of file |
There was a problem hiding this comment.
🛠️ Refactor suggestion
한글(오전/오후) 포맷 미지원으로 NaN:NaN 발생 가능
현재 AlarmsType 기본값(예: 오전 9시, 오후 8시)을 normalizeTime에 전달하면 정규식이 매칭되지 않아 parseInt가 NaN을 반환, 결과가 "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.
| 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'.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
apps/client/src/shared/apis/queries.ts (2)
2-2: 타입 전용 import로 분리 권장타입(
postSignUpRequest)을 값과 함께 동일 import 구문으로 가져오면 트랜스파일 단계에서 런타임 잔재가 남는 설정/툴체인 케이스가 있습니다. 타입은 별도import type으로 분리해 안전하게 제거되도록 해주세요.-import { getDashboardCategories, postCategory, postSignUp, postSignUpRequest } from '@shared/apis/axios'; +import { getDashboardCategories, postCategory, postSignUp } from '@shared/apis/axios'; +import type { postSignUpRequest } from '@shared/apis/axios';
41-44: 민감 정보 로그 노출 가능성 — 프로덕션 로그 제거/가드성공/실패 로그가 전체 응답 객체(토큰 포함 가능)를 그대로 출력합니다. 프로덕션에서는 제거하거나 환경 가드를 두고, 메시지 수준으로 축약하세요. 에러 핸들러의 매개변수도
AxiosError로 타이핑합니다.- console.log("회원가입 성공:", data); + if (process.env.NODE_ENV !== 'production') { + console.log('회원가입 성공'); + } ... - onError: (error) => { - console.error("회원가입 실패:", error); + onError: (error: AxiosError) => { + if (process.env.NODE_ENV !== 'production') { + console.error('회원가입 실패:', error?.message); + }
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/client/src/shared/apis/queries.ts(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/client/src/shared/apis/queries.ts (1)
apps/client/src/shared/apis/axios.ts (2)
postSignUpRequest(24-28)postSignUp(30-33)
🪛 ast-grep (0.38.6)
apps/client/src/shared/apis/queries.ts
[warning] 36-36: Detected potential storage of sensitive information in browser localStorage. Sensitive data like email addresses, personal information, or authentication tokens should not be stored in localStorage as it's accessible to any script.
Context: localStorage.setItem("token", newToken)
Note: [CWE-312] Cleartext Storage of Sensitive Information [REFERENCES]
- https://owasp.org/www-community/vulnerabilities/HTML5_Security_Cheat_Sheet
- https://cwe.mitre.org/data/definitions/312.html
(browser-storage-sensitive-data)
🔇 Additional comments (1)
apps/client/src/shared/apis/queries.ts (1)
34-38: 액세스 토큰을 localStorage에 저장하지 마세요 — httpOnly 쿠키 전환 권장파일: apps/client/src/shared/apis/queries.ts (약 34–38행)
- 문제: localStorage는 JS에서 접근 가능해 XSS 시 토큰 탈취 위험이 큼.
- 권장: 백엔드에서 httpOnly+Secure 쿠키 패턴으로 전환(권장: Access는 메모리, Refresh는 httpOnly).
- 단기완화(필수 권장): 토큰 키 네임스페이스 변경(예: "PINBACK_TOKEN"), localStorage.setItem을 try/catch로 감싸기, NODE_ENV 기반 노출 최소화, 그리고 토큰을 읽는 모든 지점(axios 인터셉터 / Authorization 헤더)을 레포 전체에서 찾아 교체 또는 검토.
- 검증 필요: 제공하신 스크립트가 rg 옵션 오류(--apps)로 실패했습니다. 레포 전체에서 token 사용처(인터셉터·Authorization 헤더·localStorage.getItem('token'))를 재검색한 결과를 올려주세요.
| export const usePostSignUp = () => { | ||
| return useMutation({ | ||
| mutationFn: (data: postSignUpRequest) => postSignUp(data), | ||
| onSuccess: (data) => { |
There was a problem hiding this comment.
🛠️ Refactor suggestion
useMutation 제네릭 지정으로 응답 스키마 고정 및 안전성 확보
현재 onSuccess의 data는 암시적으로 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.
constantly-dev
left a comment
There was a problem hiding this comment.
굿굿 수고하셨습니다~~!! 이제 FCM토큰 받아오는 로직만 추가되면 되겠네요!!
끝이 보이니까! 화이팅~~ 👍
| postSignData({ | ||
| "email": "tesdfdfsst@gmail.com", | ||
| "remindDefault": remindTime, | ||
| "fcmToken": "adlfdjlajlkadfsjlkfdsdfsdfsdfsdfsa" | ||
| }, |
There was a problem hiding this comment.
key에 string으로 저렇게 작성해도 잘 동작하나요??
추가로 email은 이후에 크롬에서 받아오는 로직이 추가되는거죠??
There was a problem hiding this comment.
넵네! 첫 회원가입 로직이라서,
정보 중복에러뜨지 않게 계속 그때 그때 이메일이랑 토큰 수정해가며 요청 보내면 성공으로 잘 뜹니다!
apps/client/src/shared/apis/axios.ts
Outdated
| const response = await apiRequest.post('/api/v1/auth/signup', data); | ||
| return response.data; |
There was a problem hiding this comment.
이렇게도 가능하고 받을 때 바로 구조 분해 할당도 가능합니다!
| const response = await apiRequest.post('/api/v1/auth/signup', data); | |
| return response.data; | |
| const { data } = await apiRequest.post('/api/v1/auth/signup', data); | |
| return data; |
| localStorage.setItem("token", newToken); | ||
| } | ||
|
|
||
| console.log("회원가입 성공:", data); |
📌 Related Issues
📄 Tasks
⭐ PR Point (To Reviewer)
현재는 임의 로우데이터로 api 연결했습니당
📷 Screenshot
Summary by CodeRabbit
신기능
개선