From b6fb21ae9511766b7c437738c35e4a8609087188 Mon Sep 17 00:00:00 2001 From: hannah0352 Date: Sat, 20 Sep 2025 03:19:54 +0900 Subject: [PATCH 1/9] =?UTF-8?q?refactor:=20=EC=A0=84=EC=97=AD=20=ED=97=A4?= =?UTF-8?q?=EB=8D=94=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/bookmarks/page.tsx | 172 +------------------------------------ 1 file changed, 2 insertions(+), 170 deletions(-) diff --git a/src/app/bookmarks/page.tsx b/src/app/bookmarks/page.tsx index 05b1749..3428d3a 100644 --- a/src/app/bookmarks/page.tsx +++ b/src/app/bookmarks/page.tsx @@ -1,18 +1,13 @@ 'use client'; import { useState, useEffect } from 'react'; -import { useRouter } from 'next/navigation'; -import Link from 'next/link'; import { Bookmark, BookmarkFormData } from '@/types/bookmark'; import { BookmarkAPI } from '@/libs/api/bookmarks'; import BookmarkItem from '@/components/bookmarks/BookmarkItem'; import BookmarkModal from '@/components/bookmarks/BookmarkModal'; -import LoginModal from '@/components/auth/LoginModal'; -import SignupModal from '@/components/auth/SignupModal'; import ConfirmModal from '@/components/ui/ConfirmModal'; export default function BookmarksPage() { - const router = useRouter(); const [bookmarks, setBookmarks] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -20,13 +15,9 @@ export default function BookmarksPage() { const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid'); // Auth states - const [signupOpen, setSignupOpen] = useState(false); - const [loginOpen, setLoginOpen] = useState(false); const [isAuthed, setIsAuthed] = useState(false); - const [userName, setUserName] = useState(undefined); const [confirmOpen, setConfirmOpen] = useState(false); const [selectedBookmark, setSelectedBookmark] = useState(null); - const [logoutConfirmOpen, setLogoutConfirmOpen] = useState(false); // 모달 상태 const [isModalOpen, setIsModalOpen] = useState(false); @@ -36,10 +27,8 @@ export default function BookmarksPage() { // 새로고침 시에도 로그인 유지 useEffect(() => { const at = localStorage.getItem('accessToken'); - const name = localStorage.getItem('userName') || undefined; if (at) { setIsAuthed(true); - setUserName(name); loadBookmarks(); } else { setLoading(false); @@ -60,9 +49,8 @@ export default function BookmarksPage() { localStorage.removeItem('refreshToken'); localStorage.removeItem('userName'); setIsAuthed(false); - setUserName(undefined); setBookmarks([]); - setLoginOpen(true); + alert('로그인이 만료되었습니다. 다시 로그인해주세요.'); } else { setError(msg); } @@ -83,7 +71,7 @@ export default function BookmarksPage() { // 북마크 추가 const handleAdd = () => { if (!isAuthed) { - setLoginOpen(true); + alert('로그인이 필요합니다.'); return; } setEditingBookmark(undefined); @@ -156,17 +144,6 @@ export default function BookmarksPage() { } }; - const handleLogout = () => { - localStorage.removeItem('accessToken'); - localStorage.removeItem('refreshToken'); - localStorage.removeItem('userName'); - setIsAuthed(false); - setUserName(undefined); - setLogoutConfirmOpen(false); - setBookmarks([]); - alert('로그아웃 되었습니다.'); - router.push('/'); - }; if (loading) { return ( @@ -178,54 +155,6 @@ export default function BookmarksPage() { return (
- {/* 헤더 */} -
-
- -
- ⏰ -
- Check Time - - - - -
- {isAuthed ? ( - <> - 안녕하세요, {userName}님 - - - ) : ( - <> - - - - )} -
-
-
- {/* 메인 컨텐츠 */}
{/* 컨트롤 바 */} @@ -282,20 +211,6 @@ export default function BookmarksPage() { 로그인이 필요합니다

북마크 기능을 사용하려면 로그인해주세요

-
- - -
) : error ? (
@@ -346,79 +261,6 @@ export default function BookmarksPage() { isLoading={modalLoading} /> - {/* 로그인 모달 */} - setLoginOpen(false)} - onSignupClick={() => { - setLoginOpen(false); - setSignupOpen(true); - }} - onSubmit={async ({ email, password }) => { - try { - const res = await fetch( - `${process.env.NEXT_PUBLIC_API_BASE}/api/auth/login`, - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email, password }), - }, - ); - - const data = await res.json(); - - if (!res.ok) throw new Error(data.error || '로그인 실패'); - - localStorage.setItem('accessToken', data.data.accessToken); - localStorage.setItem('refreshToken', data.data.refreshToken); - if (data?.data?.user?.username) { - localStorage.setItem('userName', data.data.user.username); - setUserName(data.data.user.username); - } - setIsAuthed(true); - setLoginOpen(false); - loadBookmarks(); - return true; - } catch (err) { - alert(err instanceof Error ? err.message : '로그인 중 오류 발생'); - return false; - } - }} - /> - - {/* 회원가입 모달 */} - setSignupOpen(false)} - onLoginClick={() => { - setSignupOpen(false); - setLoginOpen(true); - }} - onSubmit={async ({ username, email, password }) => { - try { - const res = await fetch( - `${process.env.NEXT_PUBLIC_API_BASE}/api/auth/register`, - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ username, email, password }), - }, - ); - const data = await res.json(); - - if (!res.ok) { - throw new Error(data.error || '회원가입 실패'); - } - - console.log('회원가입 성공', data.data.user); - alert('회원가입이 완료되었습니다. 로그인 해주세요.'); - setSignupOpen(false); - setLoginOpen(true); - } catch (err) { - alert(err instanceof Error ? err.message : '회원가입 중 오류 발생'); - } - }} - /> {/* 시간확인 확인 모달 */} - {/* 로그아웃 확인 모달 */} - setLogoutConfirmOpen(false)} - />
); } \ No newline at end of file From dfe6a23685f531dab4043f4e96444a899c729c53 Mon Sep 17 00:00:00 2001 From: hannah0352 Date: Sat, 20 Sep 2025 04:10:39 +0900 Subject: [PATCH 2/9] =?UTF-8?q?fix:=20=EB=B0=B1=EC=97=94=EB=93=9C=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=EB=AA=85=EC=9C=BC=EB=A1=9C=20=EB=A7=A4?= =?UTF-8?q?=ED=95=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ClientHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ClientHeader.tsx b/src/components/ClientHeader.tsx index a77fc1d..3556f08 100644 --- a/src/components/ClientHeader.tsx +++ b/src/components/ClientHeader.tsx @@ -90,7 +90,7 @@ export default function ClientHeader() { { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ userName, email, password }), + body: JSON.stringify({ username: userName, email, password }), }, ); const data = await res.json(); From 460ee0afa0eb689bca72a27a033a4900c7628fe4 Mon Sep 17 00:00:00 2001 From: hannah0352 Date: Sat, 20 Sep 2025 05:09:10 +0900 Subject: [PATCH 3/9] =?UTF-8?q?refactor:=20=EA=B2=80=EC=83=89=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=20=EA=B4=80=EB=A0=A8=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=EB=93=A4=20=EB=B3=84=EB=8F=84=EC=9D=98=20=ED=8F=B4?= =?UTF-8?q?=EB=8D=94=EB=A1=9C=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/page.tsx | 8 ++++---- src/app/{result => search-result}/page.tsx | 8 ++++---- src/components/{ => search-result}/AlarmCountdown.tsx | 2 +- src/components/{ => search-result}/AlarmModal.tsx | 0 src/components/{ => search-result}/KoreanStandardTime.tsx | 0 src/components/{ => search-result}/ServerSearchForm.tsx | 0 6 files changed, 9 insertions(+), 9 deletions(-) rename src/app/{result => search-result}/page.tsx (98%) rename src/components/{ => search-result}/AlarmCountdown.tsx (97%) rename src/components/{ => search-result}/AlarmModal.tsx (100%) rename src/components/{ => search-result}/KoreanStandardTime.tsx (100%) rename src/components/{ => search-result}/ServerSearchForm.tsx (100%) diff --git a/src/app/page.tsx b/src/app/page.tsx index 0436421..14c8175 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,9 +1,9 @@ 'use client'; -import SearchForm from '@/components/ServerSearchForm'; +import ServerSearchForm from '@/components/search-result/ServerSearchForm'; import { useRouter } from 'next/navigation'; import React from 'react'; -import KoreanStandardTime from '@/components/KoreanStandardTime'; +import KoreanStandardTime from '@/components/search-result/KoreanStandardTime'; export default function Home() { const router = useRouter(); @@ -26,12 +26,12 @@ export default function Home() {

서버와의 시간 차이를 실시간으로 확인하고, 완벽한 타이밍으로 티켓팅에 - 성공하세요. + 성공하세요!

{/* URL Input */}
- +
{/* Current Time */}
diff --git a/src/app/result/page.tsx b/src/app/search-result/page.tsx similarity index 98% rename from src/app/result/page.tsx rename to src/app/search-result/page.tsx index 76dcf8d..f0fb315 100644 --- a/src/app/result/page.tsx +++ b/src/app/search-result/page.tsx @@ -3,11 +3,11 @@ import React, { useState, useEffect } from 'react'; import { RefreshCw, Clock, Info } from 'lucide-react'; -import AlarmModal, { AlarmData } from '@/components/AlarmModal'; +import AlarmModal, { AlarmData } from '@/components/search-result/AlarmModal'; import { useSearchParams } from 'next/navigation'; -import KoreanStandardTime from '@/components/KoreanStandardTime'; -import ServerSearchForm from '@/components/ServerSearchForm'; -import AlarmCountdown from '@/components/AlarmCountdown'; +import KoreanStandardTime from '@/components/search-result/KoreanStandardTime'; +import ServerSearchForm from '@/components/search-result/ServerSearchForm'; +import AlarmCountdown from '@/components/search-result/AlarmCountdown'; // RTTResult와 RTTData 인터페이스는 api/network/rtt에서 사용되므로, // api/time/compare가 직접 이 데이터를 반환하지 않는다면 필요 없을 수 있습니다. diff --git a/src/components/AlarmCountdown.tsx b/src/components/search-result/AlarmCountdown.tsx similarity index 97% rename from src/components/AlarmCountdown.tsx rename to src/components/search-result/AlarmCountdown.tsx index eaef765..fd9e375 100644 --- a/src/components/AlarmCountdown.tsx +++ b/src/components/search-result/AlarmCountdown.tsx @@ -2,7 +2,7 @@ 'use client'; import { useEffect, useState } from 'react'; -import { AlarmData } from './AlarmModal'; +import { AlarmData } from './search-result/AlarmModal'; interface AlarmCountdownProps { alarm: AlarmData; diff --git a/src/components/AlarmModal.tsx b/src/components/search-result/AlarmModal.tsx similarity index 100% rename from src/components/AlarmModal.tsx rename to src/components/search-result/AlarmModal.tsx diff --git a/src/components/KoreanStandardTime.tsx b/src/components/search-result/KoreanStandardTime.tsx similarity index 100% rename from src/components/KoreanStandardTime.tsx rename to src/components/search-result/KoreanStandardTime.tsx diff --git a/src/components/ServerSearchForm.tsx b/src/components/search-result/ServerSearchForm.tsx similarity index 100% rename from src/components/ServerSearchForm.tsx rename to src/components/search-result/ServerSearchForm.tsx From 69abed61180667728dedd3df1bb8ccaabb99ed69 Mon Sep 17 00:00:00 2001 From: hannah0352 Date: Sat, 20 Sep 2025 07:02:33 +0900 Subject: [PATCH 4/9] =?UTF-8?q?feat:=20=EA=B2=80=EC=83=89=20=ED=8F=BC=20Se?= =?UTF-8?q?rverSearchForm=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/page.tsx | 2 +- src/app/search-result/page.tsx | 91 +------- .../search-result/ServerSearchForm.tsx | 78 +++++-- src/libs/api/sites.ts | 221 ++++++++++++++++++ 4 files changed, 290 insertions(+), 102 deletions(-) create mode 100644 src/libs/api/sites.ts diff --git a/src/app/page.tsx b/src/app/page.tsx index 14c8175..eabc1e1 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -8,7 +8,7 @@ import KoreanStandardTime from '@/components/search-result/KoreanStandardTime'; export default function Home() { const router = useRouter(); const handleSubmit = (url: string) => { - router.push(`/result?url=${encodeURIComponent(url)}`); + router.push(`/search-result?url=${encodeURIComponent(url)}`); }; return ( diff --git a/src/app/search-result/page.tsx b/src/app/search-result/page.tsx index f0fb315..b3f7bf7 100644 --- a/src/app/search-result/page.tsx +++ b/src/app/search-result/page.tsx @@ -1,13 +1,14 @@ // 검색 결과 및 알림 화면 'use client'; -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { RefreshCw, Clock, Info } from 'lucide-react'; import AlarmModal, { AlarmData } from '@/components/search-result/AlarmModal'; import { useSearchParams } from 'next/navigation'; import KoreanStandardTime from '@/components/search-result/KoreanStandardTime'; import ServerSearchForm from '@/components/search-result/ServerSearchForm'; import AlarmCountdown from '@/components/search-result/AlarmCountdown'; +import { SiteAPI } from '@/libs/api/sites'; // RTTResult와 RTTData 인터페이스는 api/network/rtt에서 사용되므로, // api/time/compare가 직접 이 데이터를 반환하지 않는다면 필요 없을 수 있습니다. @@ -349,73 +350,10 @@ export default function CheckTimeApp() { } }; - // Site 인터페이스 - interface Site { - id: number; - url: string; - name: string; - category: string; - description?: string; - keywords: string[]; - usage_count: number; - optimal_offset: number; - average_rtt: number; - success_rate: number; - } - - // 키워드로 사이트 검색해서 첫 번째 결과의 URL 가져오기 - const getUrlFromKeyword = async (keyword: string): Promise => { - try { - console.log(`키워드 검색 시작: "${keyword}"`); - - const response = await fetch( - `http://localhost:3001/api/sites?search=${encodeURIComponent( - keyword.trim(), - )}&limit=5`, - ); - const data = await response.json(); - - console.log('검색 API 응답:', data); + // Site 인터페이스는 이미 import됨 - if (data.success && data.data.sites && data.data.sites.length > 0) { - const sites: Site[] = data.data.sites; - - // 모든 검색 결과 로그 출력 - console.log(`"${keyword}" 검색 결과 (${sites.length}개):`); - sites.forEach((site: Site, index: number) => { - console.log(` ${index + 1}. ${site.name} - ${site.url}`); - console.log(` 키워드: [${site.keywords?.join(', ') || '없음'}]`); - }); - - // 키워드와 정확히 매치되는 사이트 찾기 - const exactMatch = sites.find((site: Site) => - site.keywords?.some( - (kw: string) => kw.toLowerCase() === keyword.toLowerCase(), - ), - ); - - if (exactMatch) { - console.log(`정확 매치 발견: ${exactMatch.name} - ${exactMatch.url}`); - return exactMatch.url; - } - // 정확 매치가 없으면 첫 번째 결과 사용 - const firstResult = sites[0]; - console.log( - `정확 매치 없음. 첫 번째 결과 사용: ${firstResult.name} - ${firstResult.url}`, - ); - return firstResult.url; - } - - console.log(`"${keyword}" 검색 결과 없음`); - return null; - } catch (error) { - console.error('사이트 검색 실패:', error); - return null; - } - }; - - const handleSubmit = async (input: string) => { + const handleSubmit = useCallback(async (input: string) => { setIsLoading(true); setServerTimeData(null); @@ -425,13 +363,13 @@ export default function CheckTimeApp() { let finalUrl = input.trim(); - // URL 형식이 아니면 키워드로 검색 + // 백엔드 API에서 URL/키워드 검색 처리 if (!isValidUrl(finalUrl)) { console.log(`키워드 검색 시작: "${finalUrl}"`); - const foundUrl = await getUrlFromKeyword(finalUrl); - - if (foundUrl) { - finalUrl = foundUrl; + const searchResult = await SiteAPI.searchSites(finalUrl, true); + + if (searchResult.results && searchResult.results.length > 0) { + finalUrl = searchResult.results[0].url; console.log(`검색 성공: ${input} → ${finalUrl}`); } else { setServerTimeData({ @@ -505,7 +443,7 @@ export default function CheckTimeApp() { } finally { setIsLoading(false); } - }; + }, []); useEffect(() => { const listener = (e: Event) => { @@ -525,7 +463,7 @@ export default function CheckTimeApp() { } document.addEventListener('toggleMilliseconds', listener); return () => document.removeEventListener('toggleMilliseconds', listener); - }, [initialUrl]); + }, [initialUrl, handleSubmit]); const handleRefresh = () => { if (serverTimeData) { @@ -535,14 +473,9 @@ export default function CheckTimeApp() { return (
- {/* 헤더 */} -
-

Check Time

-
- {/* 서버 시간 검색 폼 */}
- handleSubmit(input)} /> +

diff --git a/src/components/search-result/ServerSearchForm.tsx b/src/components/search-result/ServerSearchForm.tsx index 1dfacad..eaeeedd 100644 --- a/src/components/search-result/ServerSearchForm.tsx +++ b/src/components/search-result/ServerSearchForm.tsx @@ -5,6 +5,7 @@ import { useState } from 'react'; import { useRouter } from 'next/navigation'; import { Input } from '@/components/ui/Input'; import { Button } from '@/components/ui/Button'; +import { SiteAPI } from '@/libs/api/sites'; interface ServerSearchFormProps { onSubmit?: (url: string) => void; @@ -12,35 +13,68 @@ interface ServerSearchFormProps { export default function ServerSearchForm({ onSubmit }: ServerSearchFormProps) { const [url, setUrl] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); const router = useRouter(); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if (!url.trim()) return; + if (!url.trim() || isLoading) return; - onSubmit?.(url.trim()); - router.push(`/result?url=${encodeURIComponent(url.trim())}`); + setError(null); + setIsLoading(true); + + try { + // 백엔드 API에서 URL/키워드 검색 처리 + const searchResult = await SiteAPI.searchSites(url.trim(), true); + + if (searchResult.results && searchResult.results.length > 0) { + const finalUrl = searchResult.results[0].url; + console.log(`검색 성공: ${url} → ${finalUrl}`); + + // 검색된 URL로 이동 + onSubmit?.(finalUrl); + router.push(`/search-result?url=${encodeURIComponent(finalUrl)}`); + } else { + setError(`"${url}"에 대한 검색 결과가 없습니다.\n\n사용 가능한 키워드 예시:\n- 숭실대, SSU, 수강신청\n- 인터파크, 티켓, 콘서트\n- 무신사, 쇼핑, 패션\n- 예스24, 책, 도서\n- 지마켓, 쇼핑몰`); + } + } catch (error) { + console.error('사이트 검색 실패:', error); + setError('사이트 검색 중 오류가 발생했습니다.'); + } finally { + setIsLoading(false); + } }; return ( -
- setUrl(e.target.value)} - placeholder="서버 시간 확인이 필요한 url 주소를 검색해 보세요" - className="flex-1 bg-white shadow-md placeholder:text-gray-400 py-4 text-lg h-14" - /> - - -
+ setUrl(e.target.value)} + placeholder="서버 시간 확인이 필요한 url 주소를 검색해 보세요" + className="flex-1 bg-white shadow-md placeholder:text-gray-400 py-4 text-lg h-14" + /> + + + + + {/* 에러 메시지 표시 */} + {error && ( +
+ {error} +
+ )} +
); } diff --git a/src/libs/api/sites.ts b/src/libs/api/sites.ts new file mode 100644 index 0000000..09a5c41 --- /dev/null +++ b/src/libs/api/sites.ts @@ -0,0 +1,221 @@ +import { AuthUtils } from '@/libs/auth'; + +const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE; + +export interface Site { + id: number; + url: string; + name: string; + category: string; + description?: string; + keywords: string[]; + usage_count: number; + average_rtt: number; + success_rate: number; + similarity?: number; + matchReason?: string; + isNewlyRegistered?: boolean; +} + +export interface SiteSearchResult { + searchTerm: string; + results: Site[]; + totalFound: number; + koreanMapping?: { + korean_name: string; + actual_url: string; + similarity_threshold: number; + }; + autoDiscovery?: { + discovered: boolean; + attempted: boolean; + newSite?: Site; + source?: string; + }; + bestSimilarityFromDb: number; + searchedAt: string; +} + +export interface SiteSearchResponse { + success: boolean; + data: SiteSearchResult; + error?: string; +} + +export class SiteAPI { + /** + * 사이트 검색 (백엔드 SiteService 활용) + */ + static async searchSites( + searchTerm: string, + autoDiscover: boolean = true + ): Promise { + try { + const response = await fetch( + `${API_BASE_URL}/api/sites/search?q=${encodeURIComponent(searchTerm)}&auto_discover=${autoDiscover}`, + { + headers: AuthUtils.getAuthHeaders(), + } + ); + + if (!response.ok) { + throw new Error(`사이트 검색 실패: ${response.status} ${response.statusText}`); + } + + const result: SiteSearchResponse = await response.json(); + + if (!result.success) { + throw new Error(result.error || '사이트 검색 중 오류가 발생했습니다.'); + } + + return result.data; + } catch (error) { + console.error('사이트 검색 API 호출 실패:', error); + throw new Error('백엔드 서버에 연결할 수 없습니다. 서버가 실행 중인지 확인해주세요.'); + } + } + + + /** + * 모든 사이트 조회 (페이징 지원) + */ + static async getAllSites( + page: number = 1, + limit: number = 20, + category?: string, + sortBy: string = 'usage_count' + ): Promise<{ + sites: Site[]; + pagination: { + currentPage: number; + totalPages: number; + totalCount: number; + hasNext: boolean; + hasPrev: boolean; + }; + }> { + const params = new URLSearchParams({ + page: page.toString(), + limit: limit.toString(), + sortBy, + }); + + if (category) { + params.append('category', category); + } + + const response = await fetch( + `${API_BASE_URL}/api/sites?${params.toString()}`, + { + headers: AuthUtils.getAuthHeaders(), + } + ); + + if (!response.ok) { + throw new Error('사이트 목록을 가져오는데 실패했습니다.'); + } + + const result = await response.json(); + + if (!result.success) { + throw new Error(result.error || '사이트 목록 조회 중 오류가 발생했습니다.'); + } + + return result.data; + } + + /** + * 인기 사이트 조회 + */ + static async getPopularSites( + limit: number = 10, + category?: string + ): Promise { + const params = new URLSearchParams({ + limit: limit.toString(), + }); + + if (category) { + params.append('category', category); + } + + const response = await fetch( + `${API_BASE_URL}/api/sites/popular?${params.toString()}`, + { + headers: AuthUtils.getAuthHeaders(), + } + ); + + if (!response.ok) { + throw new Error('인기 사이트를 가져오는데 실패했습니다.'); + } + + const result = await response.json(); + + if (!result.success) { + throw new Error(result.error || '인기 사이트 조회 중 오류가 발생했습니다.'); + } + + return result.data; + } + + /** + * 카테고리 목록 조회 + */ + static async getCategories(): Promise<{ + category: string; + site_count: number; + avg_success_rate: number; + }[]> { + const response = await fetch(`${API_BASE_URL}/api/sites/categories`, { + headers: AuthUtils.getAuthHeaders(), + }); + + if (!response.ok) { + throw new Error('카테고리 목록을 가져오는데 실패했습니다.'); + } + + const result = await response.json(); + + if (!result.success) { + throw new Error(result.error || '카테고리 목록 조회 중 오류가 발생했습니다.'); + } + + return result.data; + } + + /** + * URL 자동 보정 제안 + */ + static async suggestUrlCorrection(inputUrl: string): Promise<{ + inputUrl: string; + correctedUrl: string | null; + suggestions: Array<{ + originalUrl: string; + siteName: string; + similarity: number; + }>; + hasSuggestions: boolean; + }> { + const response = await fetch(`${API_BASE_URL}/api/sites/suggest-correction`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...AuthUtils.getAuthHeaders(), + }, + body: JSON.stringify({ inputUrl }), + }); + + if (!response.ok) { + throw new Error('URL 보정 제안을 가져오는데 실패했습니다.'); + } + + const result = await response.json(); + + if (!result.success) { + throw new Error(result.error || 'URL 보정 제안 중 오류가 발생했습니다.'); + } + + return result.data; + } +} From e84176a43c9e984644c774f42fe08c18aa6e82e0 Mon Sep 17 00:00:00 2001 From: hannah0352 Date: Sat, 20 Sep 2025 07:33:32 +0900 Subject: [PATCH 5/9] =?UTF-8?q?style:=20ServerSearchForm=20=EB=94=94?= =?UTF-8?q?=EC=9E=90=EC=9D=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/search-result/page.tsx | 2 +- src/components/search-result/ServerSearchForm.tsx | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/search-result/page.tsx b/src/app/search-result/page.tsx index b3f7bf7..71e6a88 100644 --- a/src/app/search-result/page.tsx +++ b/src/app/search-result/page.tsx @@ -472,7 +472,7 @@ export default function CheckTimeApp() { }; return ( -
+
{/* 서버 시간 검색 폼 */}
diff --git a/src/components/search-result/ServerSearchForm.tsx b/src/components/search-result/ServerSearchForm.tsx index eaeeedd..308a664 100644 --- a/src/components/search-result/ServerSearchForm.tsx +++ b/src/components/search-result/ServerSearchForm.tsx @@ -47,23 +47,23 @@ export default function ServerSearchForm({ onSubmit }: ServerSearchFormProps) { }; return ( -
+
setUrl(e.target.value)} placeholder="서버 시간 확인이 필요한 url 주소를 검색해 보세요" - className="flex-1 bg-white shadow-md placeholder:text-gray-400 py-4 text-lg h-14" + className="flex-1 bg-white placeholder:text-gray-400 py-4 text-lg h-14 min-w-0" /> @@ -71,7 +71,7 @@ export default function ServerSearchForm({ onSubmit }: ServerSearchFormProps) { {/* 에러 메시지 표시 */} {error && ( -
+
{error}
)} From 82883b08eca0eb6a7aec8d5aa260cc0097acc00b Mon Sep 17 00:00:00 2001 From: hannah0352 Date: Sat, 20 Sep 2025 08:09:46 +0900 Subject: [PATCH 6/9] =?UTF-8?q?refactor:=20=EC=84=9C=EB=B2=84=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EA=B2=B0=EA=B3=BC=20ServerTimeResult=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/search-result/page.tsx | 299 +---------- .../search-result/ServerTimeResult.tsx | 476 ++++++++++++++++++ 2 files changed, 478 insertions(+), 297 deletions(-) create mode 100644 src/components/search-result/ServerTimeResult.tsx diff --git a/src/app/search-result/page.tsx b/src/app/search-result/page.tsx index 71e6a88..6e44b9c 100644 --- a/src/app/search-result/page.tsx +++ b/src/app/search-result/page.tsx @@ -2,12 +2,11 @@ 'use client'; import React, { useState, useEffect, useCallback } from 'react'; -import { RefreshCw, Clock, Info } from 'lucide-react'; -import AlarmModal, { AlarmData } from '@/components/search-result/AlarmModal'; +import { AlarmData } from '@/components/search-result/AlarmModal'; import { useSearchParams } from 'next/navigation'; import KoreanStandardTime from '@/components/search-result/KoreanStandardTime'; import ServerSearchForm from '@/components/search-result/ServerSearchForm'; -import AlarmCountdown from '@/components/search-result/AlarmCountdown'; +import ServerTimeResult, { ServerTimeData } from '@/components/search-result/ServerTimeResult'; import { SiteAPI } from '@/libs/api/sites'; // RTTResult와 RTTData 인터페이스는 api/network/rtt에서 사용되므로, @@ -28,302 +27,8 @@ import { SiteAPI } from '@/libs/api/sites'; // results: RTTResult[]; // } -interface ServerTimeData { - url: string; - serverTime?: string; // 실제 타겟 서버의 보정된 시간 - clientTime: string; // 클라이언트 요청 시작 시간 - timeDifference?: number; // 우리 서버 시간 - 타겟 서버 시간 (보정 후) - networkDelay?: number; // RTT의 절반 - rtt?: number; // 왕복 지연 시간 - error?: string; - interval?: number; // 총 처리 시간 또는 다른 인터벌 (현재는 총 처리 시간 사용) - // api/time/compare의 응답 구조를 반영하기 위한 추가 필드 - timeComparison?: { - ourServerTime: string; - targetServerTime: string; - correctedTargetTime: string; - timeDifference: number; - timeDifferenceFormatted: string; - direction: string; - }; - networkInfo?: { - rtt: number; - networkDelay: number; - reliability: string; - }; - analysis?: { - accuracy: string; - recommendation: string; - trustLevel: number; - }; - metadata?: { - measuredAt: string; - ntpSyncStatus: string; - ntpAccuracy: string; - }; -} - -function TimeDisplay({ - time, - label, - showMilliseconds = true, -}: { - time: string; - label: string; - showMilliseconds?: boolean; -}) { - const [hours, minutes, seconds, milliseconds] = time.split(/[:.]/); - - return ( -
-
{label}
-
- {hours} - : - {minutes} - : - {seconds} - {/* Milliseconds text is conditionally rendered based on showMilliseconds prop */} - {showMilliseconds && ( - <> - : - {milliseconds} - - )} - {/* The checkbox is always rendered */} - - 밀리초 - - typeof window !== 'undefined' && - document.dispatchEvent( - new CustomEvent('toggleMilliseconds', { - detail: e.target.checked, - }), - ) - } - className="w-4 h-4" - /> - -
-
- ); -} - -function ServerTimeResult({ - data, - onRefresh, - showMilliseconds, - alarmData, - onAlarmConfirm, -}: { - data: ServerTimeData; - onRefresh: () => void; - showMilliseconds: boolean; - alarmData?: AlarmData | null; // 알림 데이터가 있을 경우에만 AlarmCountdown 사용(선택적 props) - onAlarmConfirm: (data: AlarmData) => void; // 알림 설정 완료 핸들러 -}) { - const [currentServerTime, setCurrentServerTime] = useState(null); - const [mounted, setMounted] = useState(false); - const [showModal, setShowModal] = useState(false); - - // 모달 배경 클릭 시 닫기 - const handleClose = () => { - setShowModal(false); - }; - useEffect(() => { - setMounted(true); - - // 서버 시간이 있으면 실시간 업데이트 시작 - if (data.serverTime && !data.error) { - const serverBaseTime = new Date(data.serverTime); - const clientBaseTime = new Date(data.clientTime); - const timeDiff = serverBaseTime.getTime() - clientBaseTime.getTime(); - - // 초기 시간 설정 - setCurrentServerTime(new Date(Date.now() + timeDiff)); - - // 10ms마다 업데이트 - const timer = setInterval(() => { - setCurrentServerTime(new Date(Date.now() + timeDiff)); - }, 10); - - return () => clearInterval(timer); - } - }, [data.serverTime, data.clientTime, data.error]); - - const formatTime = (date: Date) => { - const hours = String(date.getHours()).padStart(2, '0'); - const minutes = String(date.getMinutes()).padStart(2, '0'); - const seconds = String(date.getSeconds()).padStart(2, '0'); - const millis = String(date.getMilliseconds()).padStart(3, '0'); - return `${hours}:${minutes}:${seconds}.${millis}`; - }; - const getTimeDifferenceText = (diff: number | undefined) => { - if (diff === null || diff === undefined) { - return '계산 불가'; - } - - const absDiff = Math.abs(diff); - const direction = diff > 0 ? '' : ''; - - if (absDiff < 1000) { - return `${direction}${diff.toFixed(2)}ms`; - } else { - return `${direction}${(diff / 1000).toFixed(2)}초 (±${( - absDiff / 1000 - ).toFixed(2)}초)`; - } - }; - - // 서버 시간 데이터 유효성 검사 - const hasServerTime = - data.serverTime && data.serverTime !== 'N/A' && !data.error; - const serverUrl = new URL(data.url); - const serverName = serverUrl.hostname; - - if (data.error) { - return ( -
-
❌ 오류 발생
-

{data.error}

- -
- ); - } - - return ( -
- {/* 서버 정보 */} -
-
- {serverName} - ({data.url}) - 서버시간 -
-
- - {/* 메인 시간 표시 */} -
- {hasServerTime && mounted && currentServerTime ? ( - - ) : hasServerTime && data.serverTime ? ( - - ) : ( -
-
- 시간 정보 없음 -
-
- 서버에서 시간 정보를 제공하지 않습니다 -
-
- )} -
- - {/* 시간 차이 정보 */} - {/* timeComparison.timeDifference를 사용하여 시간 차이 표시 */} - {data.timeComparison && - data.timeComparison.timeDifference !== undefined && - data.timeComparison.timeDifference !== null && ( -
-
-
-
- {serverName} 서버가 - - {getTimeDifferenceText( - Math.abs(data.timeComparison.timeDifference), - )} - - 더 - - {data.timeComparison.direction === 'ahead' - ? '빠릅니다' - : '느립니다'} - - . -
-
-
-
- )} - - {/* 알람 카운트다운 컴포넌트 */} - {alarmData && } - - {/* 새로고침 버튼 */} -
- -
- - {/* 소요시간 정보 (옵셔널) */} - {/* data.interval 대신 networkInfo.rtt를 활용할 수 있습니다 */} - {data.networkInfo && ( -
-
- - RTT: {data.networkInfo.rtt.toFixed(1)}ms - {data.networkInfo.networkDelay && ( - - 네트워크 지연: {data.networkInfo.networkDelay.toFixed(1)}ms - - )} -
-
- )} - - {/* 상세 정보 버튼 */} -
- - - {/* 모달은 별도로 렌더링 (button 밖에서) */} - {showModal && ( - - )} -
- - {/* 네트워크 정보 (작게 표시) - 이제 networkInfo 사용 */} - {data.networkInfo && ( -
-
-
신뢰도: {data.networkInfo.reliability}
- {data.analysis &&
정확도: {data.analysis.accuracy}
} -
-
- )} -
- ); -} export default function CheckTimeApp() { const [isLoading, setIsLoading] = useState(false); diff --git a/src/components/search-result/ServerTimeResult.tsx b/src/components/search-result/ServerTimeResult.tsx new file mode 100644 index 0000000..b572241 --- /dev/null +++ b/src/components/search-result/ServerTimeResult.tsx @@ -0,0 +1,476 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { RefreshCw, Clock, Info } from 'lucide-react'; +import AlarmCountdown from './AlarmCountdown'; +import AlarmModal, { AlarmData } from './AlarmModal'; + +interface ServerTimeData { + url: string; + serverTime?: string; // 실제 타겟 서버의 보정된 시간 + clientTime: string; // 클라이언트 요청 시작 시간 + timeDifference?: number; // 우리 서버 시간 - 타겟 서버 시간 (보정 후) + networkDelay?: number; // RTT의 절반 + rtt?: number; // 왕복 지연 시간 + error?: string; + interval?: number; // 총 처리 시간 또는 다른 인터벌 (현재는 총 처리 시간 사용) + // api/time/compare의 응답 구조를 반영하기 위한 추가 필드 + timeComparison?: { + ourServerTime: string; + targetServerTime: string; + correctedTargetTime: string; + timeDifference: number; + timeDifferenceFormatted: string; + direction: string; + }; + networkInfo?: { + rtt: number; + networkDelay: number; + reliability: string; + }; + analysis?: { + accuracy: string; + recommendation: string; + trustLevel: number; + }; + metadata?: { + measuredAt: string; + ntpSyncStatus: string; + ntpAccuracy: string; + }; +} + +interface TimeDisplayProps { + time: string; + label: string; + showMilliseconds?: boolean; +} + +function TimeDisplay({ time, label, showMilliseconds = true }: TimeDisplayProps) { + const [hours, minutes, seconds, milliseconds] = time.split(/[:.]/); + + return ( +
+
{label}
+
+ {hours} + : + {minutes} + : + {seconds} + {showMilliseconds && ( + <> + : + {milliseconds} + + )} + + 밀리초 + + typeof window !== 'undefined' && + document.dispatchEvent( + new CustomEvent('toggleMilliseconds', { + detail: e.target.checked, + }), + ) + } + className="w-4 h-4" + /> + +
+
+ ); +} + +interface ServerTimeResultProps { + data: ServerTimeData; + onRefresh: () => void; + showMilliseconds: boolean; + alarmData?: AlarmData | null; + onAlarmConfirm: (data: AlarmData) => void; +} + +export default function ServerTimeResult({ + data, + onRefresh, + showMilliseconds, + alarmData, + onAlarmConfirm, +}: ServerTimeResultProps) { + const [currentServerTime, setCurrentServerTime] = useState(null); + const [mounted, setMounted] = useState(false); + const [showAlarmModal, setShowAlarmModal] = useState(false); + const [showDetailModal, setShowDetailModal] = useState(false); + + // 알림 모달 닫기 + const handleAlarmClose = () => { + setShowAlarmModal(false); + }; + + // 상세 정보 모달 닫기 + const handleDetailClose = () => { + setShowDetailModal(false); + }; + + useEffect(() => { + setMounted(true); + + // 서버 시간이 있으면 실시간 업데이트 시작 + if (data.serverTime && !data.error) { + const serverBaseTime = new Date(data.serverTime); + const clientBaseTime = new Date(data.clientTime); + const timeDiff = serverBaseTime.getTime() - clientBaseTime.getTime(); + + // 초기 시간 설정 + setCurrentServerTime(new Date(Date.now() + timeDiff)); + + // 10ms마다 업데이트 + const timer = setInterval(() => { + setCurrentServerTime(new Date(Date.now() + timeDiff)); + }, 10); + + return () => clearInterval(timer); + } + }, [data.serverTime, data.clientTime, data.error]); + + const formatTime = (date: Date) => { + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + const millis = String(date.getMilliseconds()).padStart(3, '0'); + return `${hours}:${minutes}:${seconds}.${millis}`; + }; + + const getTimeDifferenceText = (diff: number | undefined) => { + if (diff === null || diff === undefined) { + return '계산 불가'; + } + + const absDiff = Math.abs(diff); + const direction = diff > 0 ? '' : ''; + + if (absDiff < 1000) { + return `${direction}${diff.toFixed(2)}ms`; + } else { + return `${direction}${(diff / 1000).toFixed(2)}초 (±${( + absDiff / 1000 + ).toFixed(2)}초)`; + } + }; + + // 서버 시간 데이터 유효성 검사 + const hasServerTime = + data.serverTime && data.serverTime !== 'N/A' && !data.error; + const serverUrl = new URL(data.url); + const serverName = serverUrl.hostname; + + if (data.error) { + return ( +
+
❌ 오류 발생
+

{data.error}

+ +
+ ); + } + + return ( +
+ {/* 서버 정보 */} +
+
+ {serverName} + ({data.url}) + 서버시간 +
+
+ + {/* 메인 시간 표시 */} +
+ {hasServerTime && mounted && currentServerTime ? ( + + ) : hasServerTime && data.serverTime ? ( + + ) : ( +
+
+ 시간 정보 없음 +
+
+ 서버에서 시간 정보를 제공하지 않습니다 +
+
+ )} +
+ + {/* 시간 차이 정보 */} + {data.timeComparison && + data.timeComparison.timeDifference !== undefined && + data.timeComparison.timeDifference !== null && ( +
+
+
+
+ {serverName} 서버가 + + {getTimeDifferenceText( + Math.abs(data.timeComparison.timeDifference), + )} + + 더 + + {data.timeComparison.direction === 'ahead' + ? '빠릅니다' + : '느립니다'} + + . +
+
+
+
+ )} + + {/* 알람 카운트다운 컴포넌트 */} + {alarmData && } + + {/* 새로고침 버튼 */} +
+ +
+ + {/* 소요시간 정보 */} + {data.networkInfo && ( +
+
+ + RTT: {data.networkInfo.rtt.toFixed(1)}ms + {data.networkInfo.networkDelay && ( + + 네트워크 지연: {data.networkInfo.networkDelay.toFixed(1)}ms + + )} +
+
+ )} + + {/* 알림 설정 버튼 */} +
+ + + {/* 알림 모달 */} + {showAlarmModal && ( + + )} +
+ + {/* 상세 정보 버튼 */} +
+ +
+ + {/* 상세 정보 모달 */} + {showDetailModal && ( +
+
e.stopPropagation()} + > +
+

상세 정보

+ +
+ +
+ {/* 기본 정보 */} +
+

+ 기본 정보 +

+
+
+ 서버 URL: + {data.url} +
+
+ 측정 시간: + + {data.metadata?.measuredAt || 'N/A'} + +
+
+
+ + {/* 시간 비교 정보 */} + {data.timeComparison && ( +
+

+ 시간 비교 +

+
+
+ 우리 서버 시간: + + {data.timeComparison.ourServerTime} + +
+
+ 타겟 서버 시간: + + {data.timeComparison.targetServerTime} + +
+
+ 보정된 타겟 시간: + + {data.timeComparison.correctedTargetTime} + +
+
+ 시간 차이: + + {data.timeComparison.timeDifferenceFormatted} + +
+
+ 방향: + + {data.timeComparison.direction === 'ahead' + ? '타겟이 빠름' + : '타겟이 느림'} + +
+
+
+ )} + + {/* 네트워크 정보 */} + {data.networkInfo && ( +
+

+ 네트워크 정보 +

+
+
+ RTT: + + {data.networkInfo.rtt.toFixed(2)}ms + +
+
+ 네트워크 지연: + + {data.networkInfo.networkDelay.toFixed(2)}ms + +
+
+ 신뢰성: + + {data.networkInfo.reliability} + +
+
+
+ )} + + {/* 분석 정보 */} + {data.analysis && ( +
+

+ 분석 결과 +

+
+
+ 정확도: + + {data.analysis.accuracy} + +
+
+ 신뢰 수준: + + {data.analysis.trustLevel}% + +
+
+ 권장사항: + + {data.analysis.recommendation} + +
+
+
+ )} + + {/* 메타데이터 */} + {data.metadata && ( +
+

+ 메타데이터 +

+
+
+ NTP 동기화 상태: + + {data.metadata.ntpSyncStatus} + +
+
+ NTP 정확도: + + {data.metadata.ntpAccuracy} + +
+
+
+ )} +
+
+
+ )} +
+ ); +} + +export type { ServerTimeData }; From d1ae1b01fd4fc892d9ef155621a84ed0b7fe401d Mon Sep 17 00:00:00 2001 From: hannah0352 Date: Sat, 20 Sep 2025 08:57:57 +0900 Subject: [PATCH 7/9] =?UTF-8?q?style:=20ServerTimeResult=EC=9D=98=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20=EC=A0=95=EB=B3=B4=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/search-result/page.tsx | 5 ++ .../search-result/ServerSearchForm.tsx | 4 +- .../search-result/ServerTimeResult.tsx | 88 ++++++++++++------- 3 files changed, 63 insertions(+), 34 deletions(-) diff --git a/src/app/search-result/page.tsx b/src/app/search-result/page.tsx index 6e44b9c..77403c2 100644 --- a/src/app/search-result/page.tsx +++ b/src/app/search-result/page.tsx @@ -45,6 +45,10 @@ export default function CheckTimeApp() { setAlarmData(data); }; + const handleAlarmDelete = () => { + setAlarmData(null); + }; + // URL 형식인지 확인하는 함수 const isValidUrl = (str: string) => { try { @@ -204,6 +208,7 @@ export default function CheckTimeApp() { showMilliseconds={showMilliseconds} alarmData={alarmData} // 알람 데이터가 있을 경우에만 AlarmCountdown 사용 onAlarmConfirm={handleAlarmConfirm} + onAlarmDelete={handleAlarmDelete} />
)} diff --git a/src/components/search-result/ServerSearchForm.tsx b/src/components/search-result/ServerSearchForm.tsx index 308a664..80fab39 100644 --- a/src/components/search-result/ServerSearchForm.tsx +++ b/src/components/search-result/ServerSearchForm.tsx @@ -63,9 +63,9 @@ export default function ServerSearchForm({ onSubmit }: ServerSearchFormProps) {
diff --git a/src/components/search-result/ServerTimeResult.tsx b/src/components/search-result/ServerTimeResult.tsx index b572241..e088298 100644 --- a/src/components/search-result/ServerTimeResult.tsx +++ b/src/components/search-result/ServerTimeResult.tsx @@ -1,7 +1,7 @@ 'use client'; import React, { useState, useEffect } from 'react'; -import { RefreshCw, Clock, Info } from 'lucide-react'; +import { RefreshCw, Info, Bell, X } from 'lucide-react'; import AlarmCountdown from './AlarmCountdown'; import AlarmModal, { AlarmData } from './AlarmModal'; @@ -91,6 +91,7 @@ interface ServerTimeResultProps { showMilliseconds: boolean; alarmData?: AlarmData | null; onAlarmConfirm: (data: AlarmData) => void; + onAlarmDelete?: () => void; } export default function ServerTimeResult({ @@ -99,6 +100,7 @@ export default function ServerTimeResult({ showMilliseconds, alarmData, onAlarmConfirm, + onAlarmDelete, }: ServerTimeResultProps) { const [currentServerTime, setCurrentServerTime] = useState(null); const [mounted, setMounted] = useState(false); @@ -224,16 +226,16 @@ export default function ServerTimeResult({ data.timeComparison.timeDifference !== undefined && data.timeComparison.timeDifference !== null && (
-
+
-
- {serverName} 서버가 +
+ 타겟 서버가  {getTimeDifferenceText( Math.abs(data.timeComparison.timeDifference), - )} + )}  - 더 + 더  {data.timeComparison.direction === 'ahead' ? '빠릅니다' @@ -247,15 +249,36 @@ export default function ServerTimeResult({ )} {/* 알람 카운트다운 컴포넌트 */} - {alarmData && } + {alarmData && ( +
+
+
+
+ +
+
+
알람 설정됨
+
+
+ +
+ +
+ )} {/* 새로고침 버튼 */}
@@ -264,7 +287,6 @@ export default function ServerTimeResult({ {data.networkInfo && (
- RTT: {data.networkInfo.rtt.toFixed(1)}ms {data.networkInfo.networkDelay && ( @@ -276,28 +298,30 @@ export default function ServerTimeResult({ )} {/* 알림 설정 버튼 */} -
- - - {/* 알림 모달 */} - {showAlarmModal && ( - - )} -
+ {!alarmData && ( +
+ + + {/* 알림 모달 */} + {showAlarmModal && ( + + )} +
+ )} {/* 상세 정보 버튼 */}
@@ -316,9 +340,9 @@ export default function ServerTimeResult({

상세 정보

@@ -348,7 +372,7 @@ export default function ServerTimeResult({

시간 비교

-
+
우리 서버 시간: @@ -391,7 +415,7 @@ export default function ServerTimeResult({

네트워크 정보

-
+
RTT: @@ -420,7 +444,7 @@ export default function ServerTimeResult({

분석 결과

-
+
정확도: @@ -449,7 +473,7 @@ export default function ServerTimeResult({

메타데이터

-
+
NTP 동기화 상태: From 651e11f6aadd28c0195a1fb49449eed2700691da Mon Sep 17 00:00:00 2001 From: hannah0352 Date: Sat, 20 Sep 2025 09:22:52 +0900 Subject: [PATCH 8/9] =?UTF-8?q?style:=20=EC=95=8C=EB=A6=BC=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EC=B0=BD=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/search-result/AlarmModal.tsx | 287 +++++++++++++------- 1 file changed, 189 insertions(+), 98 deletions(-) diff --git a/src/components/search-result/AlarmModal.tsx b/src/components/search-result/AlarmModal.tsx index 4d18a18..0a48c58 100644 --- a/src/components/search-result/AlarmModal.tsx +++ b/src/components/search-result/AlarmModal.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState } from 'react'; +import { X } from 'lucide-react'; interface AlarmTime { hour: string; @@ -17,6 +18,7 @@ export interface AlarmOptions { export interface AlarmData { time: AlarmTime; options: AlarmOptions; + targetTime: string; // ISO string for the target time } interface AlarmModalProps { @@ -24,14 +26,79 @@ interface AlarmModalProps { onClose: () => void; } +// 시간 옵션 생성 +const generateTimeOptions = (max: number, format: (n: number) => string) => { + return Array.from({ length: max }, (_, i) => ({ + value: format(i), + label: format(i), + })); +}; + +const hours = generateTimeOptions(24, (n) => n.toString().padStart(2, '0')); +const minutes = generateTimeOptions(60, (n) => n.toString().padStart(2, '0')); +const seconds = generateTimeOptions(60, (n) => n.toString().padStart(2, '0')); + +// 토글 스위치 컴포넌트 +function ToggleSwitch({ + checked, + onChange, + label +}: { + checked: boolean; + onChange: () => void; + label: React.ReactNode; +}) { + return ( +
+
{label}
+ +
+ ); +} + +// 체크박스 컴포넌트 +function Checkbox({ + checked, + onChange, + label +}: { + checked: boolean; + onChange: () => void; + label: string; +}) { + return ( + + ); +} + export default function AlarmModal({ onConfirm, onClose }: AlarmModalProps) { - const [hour, setHour] = useState(''); - const [minute, setMinute] = useState(''); - const [second, setSecond] = useState(''); + const [hour, setHour] = useState('00'); + const [minute, setMinute] = useState('00'); + const [second, setSecond] = useState('00'); const [options, setOptions] = useState({ preAlerts: [], - sound: false, + sound: true, red: false, }); @@ -64,117 +131,141 @@ export default function AlarmModal({ onConfirm, onClose }: AlarmModalProps) { return; } - if (!hour || !minute || !second) { - alert('⏰ 시간을 모두 입력해 주세요.'); - return; - } - onConfirm({ time: { hour, minute, second }, options, + targetTime: targetTime.toISOString(), }); - alert('알림이 설정되었습니다.'); - - onClose(); // 설정 후 모달 닫기 + onClose(); }; return ( -
- 모달 배경 클릭 시 닫기 +
e.stopPropagation()} > - {/*실제 모달 박스*/} -
e.stopPropagation()} // 모달 내부 클릭 시 닫힘 방지 - > -

⏰ 알림 설정

- -
- -
- setHour(e.target.value)} - className="w-16 p-2 border rounded" - placeholder="시" - /> - : - setMinute(e.target.value)} - className="w-16 p-2 border rounded" - placeholder="분" - /> - : - setSecond(e.target.value)} - className="w-16 p-2 border rounded" - placeholder="초" - /> -
+ {/* 헤더 */} +
+
+

알림 설정

+ +
-
- -
- - - - - -
+ {/* 시간 설정 */} +
+ +
+ + : + + : +
+
+ {/* 사전 알림 설정 */} +
+ +
+ togglePreAlert(60)} + label="1분 전 알림" + /> + togglePreAlert(30)} + label="30초 전 알림" + /> + togglePreAlert(10)} + label="10초 전 알림" + /> +
+
+ + {/* 알림 옵션 */} +
+ +
+ handleToggle('sound')} + label={ +
+ 소리 +
+ } + /> + handleToggle('red')} + label="빨간색 강조" + /> +
+
+ + {/* 버튼 */} +
+
From a928c15986ab5c576f85d37fa126bece7300b162 Mon Sep 17 00:00:00 2001 From: hannah0352 Date: Sat, 20 Sep 2025 09:47:01 +0900 Subject: [PATCH 9/9] =?UTF-8?q?style:=20=EC=95=8C=EB=A6=BC=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=ED=9B=84=20=ED=83=80=EC=9D=B4=EB=A8=B8=20=EB=94=94?= =?UTF-8?q?=EC=9E=90=EC=9D=B8=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../search-result/AlarmCountdown.tsx | 26 +------- .../search-result/ServerTimeResult.tsx | 66 ++++++++++--------- 2 files changed, 38 insertions(+), 54 deletions(-) diff --git a/src/components/search-result/AlarmCountdown.tsx b/src/components/search-result/AlarmCountdown.tsx index fd9e375..9e3719d 100644 --- a/src/components/search-result/AlarmCountdown.tsx +++ b/src/components/search-result/AlarmCountdown.tsx @@ -2,7 +2,7 @@ 'use client'; import { useEffect, useState } from 'react'; -import { AlarmData } from './search-result/AlarmModal'; +import { AlarmData } from './AlarmModal'; interface AlarmCountdownProps { alarm: AlarmData; @@ -26,15 +26,6 @@ export default function AlarmCountdown({ return `${hours} : ${minutes} : ${seconds}`; }; - // 목표 시각을 시:분:초로 포맷팅 - const formatTargetTime = () => { - const { hour, minute, second } = alarm.time; - return `${hour.padStart(2, '0')}:${minute.padStart( - 2, - '0', - )}:${second.padStart(2, '0')}`; - }; - useEffect(() => { const now = new Date(); const target = new Date(); @@ -63,26 +54,15 @@ export default function AlarmCountdown({ return (
-
- {/* 안내 텍스트 */} -
- 다음 알림까지 -
- +
{/* 카운트다운 타이머 */} -
+
{remainingSeconds !== null ? remainingSeconds > 0 ? formatTime(remainingSeconds) : '⏰ 알림 시간입니다!' : '대기 중...'}
- - {/* 목표 시각 표시 */} -
- 목표시간: {formatTargetTime()}{' '} - | 한국 표준시간 -
); diff --git a/src/components/search-result/ServerTimeResult.tsx b/src/components/search-result/ServerTimeResult.tsx index e088298..2577bee 100644 --- a/src/components/search-result/ServerTimeResult.tsx +++ b/src/components/search-result/ServerTimeResult.tsx @@ -152,14 +152,11 @@ export default function ServerTimeResult({ } const absDiff = Math.abs(diff); - const direction = diff > 0 ? '' : ''; if (absDiff < 1000) { - return `${direction}${diff.toFixed(2)}ms`; + return `${absDiff.toFixed(2)}밀리초`; } else { - return `${direction}${(diff / 1000).toFixed(2)}초 (±${( - absDiff / 1000 - ).toFixed(2)}초)`; + return `${(absDiff / 1000).toFixed(2)}초`; } }; @@ -231,9 +228,7 @@ export default function ServerTimeResult({
타겟 서버가  - {getTimeDifferenceText( - Math.abs(data.timeComparison.timeDifference), - )}  + {getTimeDifferenceText(data.timeComparison.timeDifference)}  더  @@ -248,29 +243,6 @@ export default function ServerTimeResult({
)} - {/* 알람 카운트다운 컴포넌트 */} - {alarmData && ( -
-
-
-
- -
-
-
알람 설정됨
-
-
- -
- -
- )} {/* 새로고침 버튼 */}
@@ -326,6 +298,38 @@ export default function ServerTimeResult({
+ {/* 알람 카운트다운 컴포넌트 */} + {alarmData && ( +
+
+
+
+
+ +
+
+
알림 활성화
+
+ 목표시간: {new Date(alarmData.targetTime).toLocaleTimeString('ko-KR', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + })} | 한국 표준시간 +
+
+
+ +
+ +
+
+ )} + {/* 상세 정보 모달 */} {showDetailModal && (