Skip to content

Commit 5f6a6dd

Browse files
authored
(#5) GRC-02 프런트엔드 lint debt 1차 정리
* fix: clear frontend lint debt warnings * fix: address grc-02 review feedback
1 parent 9515b1e commit 5f6a6dd

18 files changed

Lines changed: 271 additions & 206 deletions

src/app/global-error.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use client"
22

3+
import Link from "next/link"
34
import { useEffect } from "react"
45
import { AlertTriangle, Home, RefreshCcw } from "lucide-react"
56
import { getCurrentLocale, translate } from "@/shared/i18n/translate"
@@ -73,13 +74,13 @@ export default function GlobalError({ error, reset }: GlobalErrorProps) {
7374
<RefreshCcw className="mr-2 h-4 w-4" />
7475
{translate("common.retry")}
7576
</button>
76-
<a
77+
<Link
7778
href="/"
7879
className="inline-flex items-center justify-center rounded-2xl h-12 px-6 font-medium bg-blue-600 text-white hover:bg-blue-700 active:scale-[0.98] transition-all"
7980
>
8081
<Home className="mr-2 h-4 w-4" />
8182
{translate("common.go-home")}
82-
</a>
83+
</Link>
8384
</div>
8485
</div>
8586
</div>

src/app/users/[username]/user-profile-client.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client"
22

3-
import { useParams, useRouter } from "next/navigation"
3+
import { useRouter } from "next/navigation"
44
import { motion } from "framer-motion"
55
import {
66
RefreshCcw,
@@ -22,12 +22,12 @@ import { TiltCard } from "@/shared/components/ui/tilt-card"
2222
import { OptimizedAvatar } from "@/shared/components/optimized-avatar"
2323
import { Button } from "@/shared/components/button"
2424
import { Skeleton } from "@/shared/components/skeleton"
25-
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/shared/components/card"
25+
import { Card, CardContent, CardTitle, CardDescription } from "@/shared/components/card"
2626
import { toast } from "sonner"
2727
import { cn } from "@/shared/lib/utils"
2828
import { getErrorMessage } from "@/shared/lib/api-client"
2929
import { useEffect, useState } from "react"
30-
import { TIER_STYLES, getTierStyle } from "@/shared/constants/tier-styles"
30+
import { TIER_STYLES } from "@/shared/constants/tier-styles"
3131
import { useI18n } from "@/shared/providers/locale-provider"
3232

3333
interface UserProfileClientProps {
@@ -45,8 +45,11 @@ export function UserProfileClient({ username }: UserProfileClientProps) {
4545

4646
useEffect(() => {
4747
if (user) {
48-
setDisplayPercentile(0)
49-
setTimeout(() => setDisplayPercentile(user.percentile), 100)
48+
const timeoutId = window.setTimeout(() => {
49+
setDisplayPercentile(user.percentile)
50+
}, 100)
51+
52+
return () => window.clearTimeout(timeoutId)
5053
}
5154
}, [user])
5255

src/features/auth/store/auth-store.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect } from 'react';
1+
import { useSyncExternalStore } from 'react';
22
import { create } from 'zustand';
33
import { persist } from 'zustand/middleware';
44
import { User } from '@/shared/types/api';
@@ -24,17 +24,15 @@ export const useAuthStore = create<AuthState>()(
2424
)
2525
);
2626

27-
export const useAuthHydrated = () => {
28-
const [hydrated, setHydrated] = useState(false);
29-
30-
useEffect(() => {
31-
if (useAuthStore.persist?.hasHydrated?.()) {
32-
setHydrated(true);
33-
return;
34-
}
35-
const unsub = useAuthStore.persist?.onFinishHydration?.(() => setHydrated(true));
36-
return () => unsub?.();
37-
}, []);
27+
const subscribeToAuthHydration = (onStoreChange: () => void) => {
28+
const unsubscribe = useAuthStore.persist?.onFinishHydration?.(() => onStoreChange());
29+
return () => unsubscribe?.();
30+
};
3831

39-
return hydrated;
32+
export const useAuthHydrated = () => {
33+
return useSyncExternalStore(
34+
subscribeToAuthHydration,
35+
() => useAuthStore.persist?.hasHydrated?.() ?? false,
36+
() => false
37+
);
4038
};

src/features/home/components/hero-section.tsx

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client"
22

3-
import { useState, useEffect, useRef, useCallback } from "react"
3+
import { useState, useRef, useCallback } from "react"
44
import { motion, AnimatePresence, useScroll, useTransform } from "framer-motion"
55
import { useRouter } from "next/navigation"
66
import { Search, History, X, BookOpen, TrendingUp } from "lucide-react"
@@ -17,6 +17,7 @@ import { LiveTicker } from "@/shared/components/ui/live-ticker"
1717
import { toast } from "sonner"
1818
import { useI18n } from "@/shared/providers/locale-provider"
1919
import { localizePathname } from "@/shared/i18n/config"
20+
import { useHasMounted } from "@/shared/hooks/use-has-mounted"
2021

2122
export function HeroSection() {
2223
const { t, locale } = useI18n()
@@ -25,10 +26,10 @@ export function HeroSection() {
2526
const [open, setOpen] = useState(false)
2627
const [isFocused, setIsFocused] = useState(false)
2728
const [query, setQuery] = useState("")
28-
const [mounted, setMounted] = useState(false)
2929
const [selectedIndex, setSelectedIndex] = useState(-1)
3030
const inputRef = useRef<HTMLInputElement>(null)
3131
const sectionRef = useRef<HTMLElement>(null)
32+
const mounted = useHasMounted()
3233
const isMobile = useIsMobile()
3334
const prefersReducedMotion = useReducedMotion()
3435

@@ -54,9 +55,8 @@ export function HeroSection() {
5455
? t("home.search.placeholder.mobile", { example: placeholderText })
5556
: t("home.search.placeholder.desktop", { example: placeholderText })
5657

57-
useEffect(() => {
58-
setMounted(true)
59-
}, [])
58+
const selectedSearch = selectedIndex >= 0 ? recentSearches[selectedIndex] ?? null : null
59+
const activeQuery = selectedSearch ?? query
6060

6161
const handleFocus = useCallback(() => {
6262
setOpen(true)
@@ -92,7 +92,7 @@ export function HeroSection() {
9292

9393
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
9494
if (!open) {
95-
if (e.key === 'Enter') handleSearch(query)
95+
if (e.key === 'Enter') handleSearch(activeQuery)
9696
return
9797
}
9898
switch (e.key) {
@@ -107,7 +107,7 @@ export function HeroSection() {
107107
case 'Enter':
108108
e.preventDefault()
109109
if (selectedIndex >= 0) handleSearch(recentSearches[selectedIndex])
110-
else handleSearch(query)
110+
else handleSearch(activeQuery)
111111
break
112112
case 'Escape':
113113
setOpen(false)
@@ -116,13 +116,7 @@ export function HeroSection() {
116116
inputRef.current?.blur()
117117
break
118118
}
119-
}, [open, query, recentSearches, selectedIndex, handleSearch])
120-
121-
useEffect(() => {
122-
if (selectedIndex >= 0 && recentSearches[selectedIndex]) {
123-
setQuery(recentSearches[selectedIndex])
124-
}
125-
}, [selectedIndex, recentSearches])
119+
}, [activeQuery, open, recentSearches, selectedIndex, handleSearch])
126120

127121
return (
128122
<section
@@ -191,7 +185,7 @@ export function HeroSection() {
191185
ref={inputRef}
192186
className="h-14 sm:h-16 w-full bg-transparent px-2 text-base sm:text-lg font-medium outline-none placeholder:text-muted-foreground/40 font-sans"
193187
placeholder={placeholder}
194-
value={query}
188+
value={activeQuery}
195189
onChange={(e) => {
196190
setQuery(e.target.value)
197191
setSelectedIndex(-1)
@@ -215,7 +209,7 @@ export function HeroSection() {
215209
<Button
216210
size="lg"
217211
className="h-10 sm:h-12 rounded-xl px-4 sm:px-6 text-sm sm:text-base font-bold bg-primary text-primary-foreground hover:bg-primary/90 shadow-lg"
218-
onClick={() => handleSearch(query)}
212+
onClick={() => handleSearch(activeQuery)}
219213
>
220214
{t("home.search.submit")}
221215
</Button>

src/features/ranking/api/ranking-service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const getRankingList = cache(async (page: number, tier?: Tier): Promise<R
1111
params.append("tier", tier)
1212
}
1313

14-
return apiClient.get<any, RankingListResponse>(`/ranking?${params.toString()}`)
14+
return apiClient.get<void, RankingListResponse>(`/ranking?${params.toString()}`)
1515
})
1616

1717
export const useRankingList = (page: number, tier?: Tier) => {
@@ -20,4 +20,4 @@ export const useRankingList = (page: number, tier?: Tier) => {
2020
queryFn: () => getRankingList(page, tier),
2121
placeholderData: (previousData) => previousData,
2222
})
23-
}
23+
}

src/features/user/components/badge-generator.tsx

Lines changed: 41 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { useState } from "react"
44
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/shared/components/card"
55
import { Button } from "@/shared/components/button"
6-
import { Check, Copy, Link2, Code2, ExternalLink } from "lucide-react"
6+
import { Check, Copy, Link2, Code2, ExternalLink, type LucideIcon } from "lucide-react"
77
import { toast } from "sonner"
88
import { cn } from "@/shared/lib/utils"
99
import { useI18n } from "@/shared/providers/locale-provider"
@@ -15,6 +15,36 @@ interface BadgeGeneratorProps {
1515
}
1616

1717
type CopyType = "markdown" | "html" | "link" | null
18+
type BadgeCopyType = Exclude<CopyType, null>
19+
20+
interface BadgeCopyButtonProps {
21+
copied: CopyType
22+
icon: LucideIcon
23+
label: string
24+
onCopy: () => void
25+
type: BadgeCopyType
26+
}
27+
28+
function BadgeCopyButton({ copied, icon: Icon, label, onCopy, type }: BadgeCopyButtonProps) {
29+
return (
30+
<Button
31+
onClick={onCopy}
32+
variant="ghost"
33+
className={cn(
34+
"h-10 px-4 rounded-xl font-medium text-sm transition-all duration-200",
35+
"bg-secondary/50 hover:bg-secondary border border-transparent",
36+
copied === type && "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20"
37+
)}
38+
>
39+
{copied === type ? (
40+
<Check className="w-4 h-4 mr-2" />
41+
) : (
42+
<Icon className="w-4 h-4 mr-2" />
43+
)}
44+
{label}
45+
</Button>
46+
)
47+
}
1848

1949
export function BadgeGenerator({ nodeId, username }: BadgeGeneratorProps) {
2050
const { t, locale } = useI18n()
@@ -27,7 +57,7 @@ export function BadgeGenerator({ nodeId, username }: BadgeGeneratorProps) {
2757
const markdownCode = `[![Git Ranker](${badgeUrl})](${profileUrl})`
2858
const htmlCode = `<a href="${profileUrl}"><img src="${badgeUrl}" alt="Git Ranker Badge" /></a>`
2959

30-
const handleCopy = async (text: string, type: CopyType) => {
60+
const handleCopy = async (text: string, type: BadgeCopyType) => {
3161
await navigator.clipboard.writeText(text)
3262
setCopied(type)
3363
toast.success(
@@ -38,35 +68,6 @@ export function BadgeGenerator({ nodeId, username }: BadgeGeneratorProps) {
3868
setTimeout(() => setCopied(null), 2000)
3969
}
4070

41-
const CopyButton = ({
42-
type,
43-
text,
44-
icon: Icon,
45-
label
46-
}: {
47-
type: CopyType
48-
text: string
49-
icon: React.ElementType
50-
label: string
51-
}) => (
52-
<Button
53-
onClick={() => handleCopy(text, type)}
54-
variant="ghost"
55-
className={cn(
56-
"h-10 px-4 rounded-xl font-medium text-sm transition-all duration-200",
57-
"bg-secondary/50 hover:bg-secondary border border-transparent",
58-
copied === type && "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20"
59-
)}
60-
>
61-
{copied === type ? (
62-
<Check className="w-4 h-4 mr-2" />
63-
) : (
64-
<Icon className="w-4 h-4 mr-2" />
65-
)}
66-
{label}
67-
</Button>
68-
)
69-
7071
return (
7172
<Card className="rounded-[2rem] sm:rounded-[2.5rem] border-0 bg-white/60 dark:bg-black/20 backdrop-blur-xl shadow-sm overflow-hidden">
7273
<CardHeader className="pb-4 px-5 sm:px-8 pt-6 sm:pt-8">
@@ -104,23 +105,26 @@ export function BadgeGenerator({ nodeId, username }: BadgeGeneratorProps) {
104105

105106
{/* Copy Buttons */}
106107
<div className="flex flex-wrap gap-2">
107-
<CopyButton
108+
<BadgeCopyButton
109+
copied={copied}
108110
type="markdown"
109-
text={markdownCode}
110111
icon={Copy}
111112
label="Markdown"
113+
onCopy={() => handleCopy(markdownCode, "markdown")}
112114
/>
113-
<CopyButton
115+
<BadgeCopyButton
116+
copied={copied}
114117
type="html"
115-
text={htmlCode}
116118
icon={Code2}
117119
label="HTML"
120+
onCopy={() => handleCopy(htmlCode, "html")}
118121
/>
119-
<CopyButton
122+
<BadgeCopyButton
123+
copied={copied}
120124
type="link"
121-
text={badgeUrl}
122125
icon={Link2}
123126
label={t("profile.badge.image-link")}
127+
onCopy={() => handleCopy(badgeUrl, "link")}
124128
/>
125129
<Button
126130
asChild

src/features/user/components/score-info-modal.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import {
44
Dialog,
55
DialogContent,
6-
DialogHeader,
76
DialogTitle,
87
} from "@/shared/components/dialog"
98
import {
@@ -146,4 +145,4 @@ export function ScoreInfoModal({ open, onOpenChange }: ScoreInfoModalProps) {
146145
</DialogContent>
147146
</Dialog>
148147
)
149-
}
148+
}

0 commit comments

Comments
 (0)