Conversation
Walkthrough검색 결과 기능을 재구성해 결과 경로를 Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant Home as Home(/)
participant Form as ServerSearchForm
participant Sites as SiteAPI
participant Router as Next Router
participant Result as /search-result
participant TimeAPI as /api/time/compare
User->>Home: 페이지 로드
Home->>Form: 렌더링 (onSubmit 제공)
User->>Form: URL 또는 키워드 입력 → 제출
Form->>Sites: searchSites(term, autoDiscover)
Sites-->>Form: SiteSearchResult (성공/실패)
alt 사이트 검색 성공
Form->>Router: push('/search-result?url=...')
Router-->>Result: 페이지 로드
Result->>TimeAPI: POST /api/time/compare(finalUrl, metadata)
TimeAPI-->>Result: 서버시간 비교 결과 및 네트워크 메타
Result-->>User: 서버시간, 차이, 세부정보 렌더링
else 실패/결과 없음
Form-->>User: 에러 메시지 노출
end
sequenceDiagram
autonumber
actor User
participant Result as ServerTimeResult
participant AModal as AlarmModal
participant ACount as AlarmCountdown
User->>Result: "알람 설정" 클릭
Result->>AModal: 모달 오픈
User->>AModal: 시/분/초 및 옵션 선택
AModal-->>Result: onConfirm(AlarmData with targetTime)
Result->>ACount: 카운트다운 시작 (알림/사전알림 트리거)
ACount-->>User: 실시간 남은시간 업데이트 / 알림 실행
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
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 |
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)
src/app/bookmarks/page.tsx (1)
112-112: 경로 변경 누락: /result → /search-result검색 결과 경로가 /search-result로 변경되었으나 아래 위치들이 여전히 /result로 열립니다. 모두 /search-result로 수정하세요.
- src/app/bookmarks/page.tsx (line 112)
- window.open(`/result?url=${encodeURIComponent(selectedBookmark.custom_url)}`, '_blank'); + window.open(`/search-result?url=${encodeURIComponent(selectedBookmark.custom_url)}`, '_blank');
- src/components/bookmarks/BookmarkList.tsx (line 89)
- window.open(`/result?url=${encodeURIComponent(selectedBookmark.custom_url)}`, '_blank'); + window.open(`/search-result?url=${encodeURIComponent(selectedBookmark.custom_url)}`, '_blank');
🧹 Nitpick comments (17)
src/components/ClientHeader.tsx (5)
78-86: 프론트 변수명도 username으로 통일 제안프론트 내부 변수도
username으로 맞추면 매핑 혼동을 줄일 수 있습니다.- async function handleSignupSubmit({ - userName, + async function handleSignupSubmit({ + username, email, password, }: { - userName: string; + username: string; email: string; password: string; }) { @@ - body: JSON.stringify({ username: userName, email, password }), + body: JSON.stringify({ username, email, password }),Also applies to: 93-94
31-41: 스토리지 동기화 범위 확장다른 탭에서
userName만 변경되는 경우 반영이 늦을 수 있습니다. 두 키 모두에 반응하도록 보완을 권장합니다.- const onStorage = (e: StorageEvent) => { - if (e.key === 'accessToken') { + const onStorage = (e: StorageEvent) => { + if (e.key === 'accessToken' || e.key === 'userName') { const has = !!localStorage.getItem('accessToken'); setIsAuthed(has); setUserName(localStorage.getItem('userName') || undefined); } };
63-67: 토큰의 localStorage 저장은 XSS에 취약 — httpOnly 쿠키 전환 검토가능하면 서버에서 httpOnly+Secure 쿠키로 발급/회수하도록 전환하세요. Next.js Route Handler에서 쿠키 설정, 클라이언트는
Authorization헤더 대신 쿠키 기반 인증으로 단순화하는 방식을 권장합니다.
51-60: 네트워크 타임아웃 부재장시간 대기 방지를 위해
AbortController기반 타임아웃(예: 10s) 적용을 고려해 주세요. 중복 제출 방지도 함께 처리하면 UX가 좋아집니다.Also applies to: 87-96
109-118: 로그아웃 후 내비게이션은 replace 권장
router.push('/')대신router.replace('/')를 쓰면 뒤로 가기로 보호된 화면으로 복귀하는 상황을 줄일 수 있습니다.- router.push('/'); + router.replace('/');src/components/search-result/AlarmCountdown.tsx (1)
29-53: alarm.targetTime을 직접 사용하고 타이머 드리프트 줄이기현재 time-of-day(hour/minute/second)로 오늘 날짜 기준 target을 재구성합니다. AlarmModal이 이미 ISO
targetTime을 넘기므로 이를 그대로 사용하면 날짜/타임존 오해를 줄일 수 있고, 매 tick마다Date.now()로 남은 시간을 계산하면 드리프트가 줄어듭니다.- useEffect(() => { - const now = new Date(); - const target = new Date(); - - target.setHours(parseInt(alarm.time.hour)); - target.setMinutes(parseInt(alarm.time.minute)); - target.setSeconds(parseInt(alarm.time.second)); - target.setMilliseconds(0); - - let seconds = Math.floor((target.getTime() - now.getTime()) / 1000); - if (seconds < 0) seconds = 0; - setRemainingSeconds(seconds); - - const interval = setInterval(() => { - seconds -= 1; - setRemainingSeconds(seconds); - if (seconds <= 0) { - clearInterval(interval); - setRemainingSeconds(0); - onComplete?.(); - } - }, 1000); - - return () => clearInterval(interval); - }, [alarm, onComplete]); + useEffect(() => { + const target = new Date(alarm.targetTime); + const update = () => { + const seconds = Math.max( + 0, + Math.floor((target.getTime() - Date.now()) / 1000), + ); + setRemainingSeconds(seconds); + }; + update(); + const id = setInterval(() => { + const seconds = Math.max( + 0, + Math.floor((target.getTime() - Date.now()) / 1000), + ); + setRemainingSeconds(seconds); + if (seconds === 0) { + clearInterval(id); + onComplete?.(); + } + }, 1000); + return () => clearInterval(id); + }, [alarm.targetTime, onComplete]);src/app/page.tsx (1)
11-11: 중복 네비게이션 가능성
ServerSearchForm내부에서도router.push를 수행합니다. 부모에서도 push하면 동일 경로로 이중 네비게이션이 발생할 수 있습니다. 폼 쪽을 “onSubmit이 있으면 push하지 않음”으로 바꾸는 것을 권장합니다. 해당 수정은 ServerSearchForm.tsx 코멘트를 참조하세요.src/app/search-result/page.tsx (3)
53-60: URL 판정 로직의 false positive 제거
catch에서startsWith('http')로 true를 반환하면 유효하지 않은 URL도 URL로 오인될 수 있습니다. 파싱이 실패하면 단순히 false를 반환하세요.- const isValidUrl = (str: string) => { + const isValidUrl = (str: string) => { try { new URL(str); return true; } catch { - return str.startsWith('http://') || str.startsWith('https://'); + return false; } };
187-188: 중복 검색/네비게이션 가능성이 페이지에서
onSubmit={handleSubmit}로 넘기면 폼 내부 검색 + 부모의 처리(그리고 폼의router.push)가 중복 수행될 수 있습니다. ServerSearchForm이onSubmit이 주어진 경우에는router.push를 하지 않도록 바꾸면 중복 호출을 방지할 수 있습니다. (ServerSearchForm.tsx 수정안 참조)
217-218: 밀리초 토글 UI 중복 제거
TimeDisplay에도 토글이 있고 여기KoreanStandardTime에도 기본 토글이 있습니다. 한 곳만 노출되도록showToggle={false}권장.- <KoreanStandardTime showMilliseconds={showMilliseconds} /> + <KoreanStandardTime showMilliseconds={showMilliseconds} showToggle={false} />src/components/search-result/ServerSearchForm.tsx (2)
36-38: 부모 전달 onSubmit과 라우팅이 중복 실행됨부모가 라우팅을 담당하도록,
onSubmit이 제공되면 내부에서router.push를 수행하지 않도록 변경하세요. 재사용성도 좋아집니다.- // 검색된 URL로 이동 - onSubmit?.(finalUrl); - router.push(`/search-result?url=${encodeURIComponent(finalUrl)}`); + // 검색된 URL 전달 및 필요 시 라우팅 + if (onSubmit) { + onSubmit(finalUrl); + } else { + router.push(`/search-result?url=${encodeURIComponent(finalUrl)}`); + }
74-76: 여러 줄 에러 메시지 가독성 개선
\n을 렌더링하려면whitespace-pre-line클래스 추가가 필요합니다.- <div className="mt-4 text-red-500 text-sm text-center max-w-6xl mx-auto"> + <div className="mt-4 text-red-500 text-sm text-center max-w-6xl mx-auto whitespace-pre-line">src/components/search-result/AlarmModal.tsx (1)
118-141: 지난 시각 처리 UX 개선 및 parseInt 기수 지정
- 지난 시각이면 경고 후 종료 대신 다음날로 rollover하면 UX가 좋아집니다.
parseInt는 기수(10)를 명시하세요.- targetTime.setHours(parseInt(hour)); - targetTime.setMinutes(parseInt(minute)); - targetTime.setSeconds(parseInt(second)); + targetTime.setHours(parseInt(hour, 10)); + targetTime.setMinutes(parseInt(minute, 10)); + targetTime.setSeconds(parseInt(second, 10)); targetTime.setMilliseconds(0); const timeUntilTarget = targetTime.getTime() - now.getTime(); - if (timeUntilTarget < 0) { - alert('❗ 이미 지난 시간입니다. 다시 설정해 주세요.'); - return; - } + if (timeUntilTarget < 0) { + // 지난 시각이면 다음날 같은 시각으로 설정 + targetTime.setDate(targetTime.getDate() + 1); + }src/components/search-result/ServerTimeResult.tsx (3)
133-135: 10ms 인터벌은 과합니다 — 30fps(≈33ms) 권장UI 업데이트는 10ms(100fps)까지 필요하지 않습니다. 33ms로 낮춰도 충분히 부드럽고 배터리/CPU 소모를 줄입니다.
- const timer = setInterval(() => { + const timer = setInterval(() => { setCurrentServerTime(new Date(Date.now() + timeDiff)); - }, 10); + }, 33);
166-168: URL 파싱 실패 시 런타임 예외 가능
new URL(data.url)은 잘못된 URL이면 throw합니다. try/catch로 감싸고 폴백을 두세요.- const serverUrl = new URL(data.url); - const serverName = serverUrl.hostname; + let serverName = data.url; + try { + serverName = new URL(data.url).hostname; + } catch { + // invalid URL일 경우 원문 표시 + }
259-267: 0값 숨김 버그: truthy 체크 대신 타입 체크 사용
networkDelay가 0이면 falsy로 처리되어 표시되지 않습니다.typeof === 'number'로 조건을 바꾸세요.- {data.networkInfo.networkDelay && ( + {typeof data.networkInfo.networkDelay === 'number' && ( <span className="ml-4"> 네트워크 지연: {data.networkInfo.networkDelay.toFixed(1)}ms </span> )}src/libs/api/sites.ts (1)
45-76: 네트워크 신뢰성 강화 제안(옵션)장시간 응답 지연에 대비해
AbortController로 타임아웃을 두면 UX가 개선됩니다. 필요 시 공통 fetch wrapper로 적용 권장.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
src/app/bookmarks/page.tsx(2 hunks)src/app/page.tsx(2 hunks)src/app/result/page.tsx(0 hunks)src/app/search-result/page.tsx(1 hunks)src/components/AlarmModal.tsx(0 hunks)src/components/ClientHeader.tsx(1 hunks)src/components/ServerSearchForm.tsx(0 hunks)src/components/search-result/AlarmCountdown.tsx(1 hunks)src/components/search-result/AlarmModal.tsx(1 hunks)src/components/search-result/ServerSearchForm.tsx(1 hunks)src/components/search-result/ServerTimeResult.tsx(1 hunks)src/libs/api/sites.ts(1 hunks)
💤 Files with no reviewable changes (3)
- src/components/ServerSearchForm.tsx
- src/components/AlarmModal.tsx
- src/app/result/page.tsx
🧰 Additional context used
🧬 Code graph analysis (5)
src/app/search-result/page.tsx (5)
src/components/search-result/ServerTimeResult.tsx (2)
ServerTimeData(504-504)ServerTimeResult(97-502)src/components/search-result/AlarmModal.tsx (1)
AlarmData(18-22)src/libs/api/sites.ts (1)
SiteAPI(45-221)src/components/search-result/ServerSearchForm.tsx (1)
ServerSearchForm(14-80)src/components/search-result/KoreanStandardTime.tsx (1)
KoreanStandardTime(5-133)
src/components/search-result/ServerSearchForm.tsx (3)
src/libs/api/sites.ts (1)
SiteAPI(45-221)src/components/ui/Input.tsx (1)
Input(5-19)src/components/ui/Button.tsx (1)
Button(4-18)
src/components/search-result/ServerTimeResult.tsx (2)
src/components/search-result/AlarmModal.tsx (2)
AlarmData(18-22)AlarmModal(94-274)src/components/search-result/AlarmCountdown.tsx (1)
AlarmCountdown(12-69)
src/app/page.tsx (1)
src/components/search-result/ServerSearchForm.tsx (1)
ServerSearchForm(14-80)
src/libs/api/sites.ts (1)
src/libs/auth.ts (1)
AuthUtils(3-37)
| // 1. /api/time/compare 엔드포인트 호출 | ||
| const compareResponse = await fetch( | ||
| `${process.env.NEXT_PUBLIC_API_BASE}/api/time/compare`, | ||
| { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ targetUrl: finalUrl }), | ||
| }, |
There was a problem hiding this comment.
환경변수 미설정 시 API 호출 실패 위험
process.env.NEXT_PUBLIC_API_BASE가 비어 있으면 undefined/api/time/compare로 호출됩니다. 빈 문자열 fallback을 두고 상대 경로로 호출되게 하세요.
- // 1. /api/time/compare 엔드포인트 호출
- const compareResponse = await fetch(
- `${process.env.NEXT_PUBLIC_API_BASE}/api/time/compare`,
- {
+ // 1. /api/time/compare 엔드포인트 호출
+ const base = process.env.NEXT_PUBLIC_API_BASE ?? '';
+ const compareResponse = await fetch(
+ `${base}/api/time/compare`,
+ {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ targetUrl: finalUrl }),
},
);📝 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.
| // 1. /api/time/compare 엔드포인트 호출 | |
| const compareResponse = await fetch( | |
| `${process.env.NEXT_PUBLIC_API_BASE}/api/time/compare`, | |
| { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ targetUrl: finalUrl }), | |
| }, | |
| // 1. /api/time/compare 엔드포인트 호출 | |
| const base = process.env.NEXT_PUBLIC_API_BASE ?? ''; | |
| const compareResponse = await fetch( | |
| `${base}/api/time/compare`, | |
| { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ targetUrl: finalUrl }), | |
| }, |
🤖 Prompt for AI Agents
In src/app/search-result/page.tsx around lines 94 to 101, the fetch uses
process.env.NEXT_PUBLIC_API_BASE directly which can be undefined and produce
"undefined/api/..."; update the code to default the env var to an empty string
and normalize it (trim and remove any trailing slash) before constructing the
URL so the call falls back to a relative path when the env var is not set.
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ userName, email, password }), | ||
| body: JSON.stringify({ username: userName, email, password }), | ||
| }, | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
회원가입 payload 키 전환(username) — 변경 자체는 적절, 호출부 일괄 검증 요청
BE가 username(소문자)로 받는 계약과 일치합니다. 같은 엔드포인트를 호출하는 다른 곳에 userName 키가 남아 있지 않은지 확인 부탁드립니다.
다음 스크립트로 전체 레포에서 점검해 주세요:
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 회원가입 엔드포인트 호출부 맥락 확인
rg -nP -C2 "api/auth/register" --type ts --type tsx
# payload에 userName 키 사용 흔적 확인
rg -nP --type ts --type tsx '\{[^}]*\buserName\s*:'
# 로그인 응답에서 user.username 사용 위치 확인
rg -nP --type ts --type tsx '\buser\.username\b'Length of output: 27
회원가입 payload 키 전환(userName → username) — 전체 호출부 재검증 필요
rg가 'unrecognized file type: tsx' 오류로 검색이 중단됐습니다. 레포 전체에서 userName 잔여와 엔드포인트 호출을 재확인하려면 아래 스크립트로 재실행하세요.
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 엔드포인트 호출 맥락 확인
rg -nP -C2 "api/auth/register" || true
# payload에 userName 키 사용 흔적 확인
rg -nP '\buserName\b' || true
# 로그인/응답에서 user.username 사용 위치 확인
rg -nP '\buser\.username\b' || true
# JSON.stringify 내부 또는 fetch 옵션에 userName/username 포함 여부 확인
rg -nP 'JSON\.stringify\([^)]*\b(?:userName|username)\b' || true
rg -nP 'fetch\([^,)]*,\s*{[^}]*\b(?:userName|username)\b' || trueLength of output: 2325
회원가입 payload 키 통일 필요 (userName → username) — 수정 필요
- 수정 대상: src/components/auth/SignupModal.tsx:70 — 현재 payload에
userName: userName.trim()사용,username으로 변경 필요. - 확인: src/components/ClientHeader.tsx는 이미
username으로 전송됨. localStorage의'userName'키(libs/auth.ts 등)는 내부 저장용으로 유지 가능하나, API로 전송되는 모든 payload 키는username으로 통일할 것.
🤖 Prompt for AI Agents
In src/components/auth/SignupModal.tsx around line 70 (and note
src/components/ClientHeader.tsx lines 91-95 is already using `username`), the
signup payload currently uses the key `userName`; change that key to `username`
when building the JSON body (e.g., username: userName.trim()) so the API payload
matches ClientHeader's `username` key; keep localStorage key 'userName'
unchanged if used only for internal storage, but ensure every network request
sends `username`.
| @@ -0,0 +1,221 @@ | |||
| import { AuthUtils } from '@/libs/auth'; | |||
|
|
|||
| const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE; | |||
There was a problem hiding this comment.
환경변수 미설정 시 모든 API가 깨짐
NEXT_PUBLIC_API_BASE가 비면 undefined/...로 호출됩니다. 빈 문자열 fallback 및 트레일링 슬래시 제거를 권장합니다.
-const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE;
+const API_BASE_URL = (process.env.NEXT_PUBLIC_API_BASE ?? '').replace(/\/$/, '');📝 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 API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE; | |
| const API_BASE_URL = (process.env.NEXT_PUBLIC_API_BASE ?? '').replace(/\/$/, ''); |
🤖 Prompt for AI Agents
In src/libs/api/sites.ts around line 3, the API base constant is assigned
directly from process.env.NEXT_PUBLIC_API_BASE which can be undefined and
produce requests like "undefined/…"; change it to default to an empty string
when the env var is missing and normalize by trimming whitespace and removing
any trailing slash so callers get a stable base (e.g. use
(process.env.NEXT_PUBLIC_API_BASE ?? '').trim().replace(/\/+$/, '') to produce a
safe API_BASE_URL).
📌 작업 내용
📸 스크린샷
📝 기타
Summary by CodeRabbit
New Features
Bug Fixes
Refactor