Jiseoup/showmycodePublic
EN|KO
  • 코드
  • 커밋
  • 풀 리퀘스트
← 목록으로

chore: update comments

JiseoupJiseoup · 2026년 4월 3일2c9323e

변경된 파일16개+42 -32

변경된 파일

+42 -32 · 16개

@@ -29,7 +29,7 @@ export default async function CommitDetailPage({ params }: Props) {
← {dict.commits.backToList}
</Link>
- {/* 커밋 헤더 */}
+ {/* Commit header. */}
<div className="border border-border rounded-lg p-4 space-y-3">
<div>
<p className="font-semibold text-base leading-snug">{title}</p>
@@ -59,7 +59,7 @@ export default async function CommitDetailPage({ params }: Props) {
</div>
</div>
- {/* 변경된 파일 */}
+ {/* Files changed. */}
<div>
<h2 className="text-sm font-semibold mb-3">
{dict.commits.filesChanged}
@@ -53,7 +53,7 @@ export default async function PullDetailPage({ params, searchParams }: Props) {
getDictionary(lang as Locale),
]);
- // 탭에 따라 필요한 데이터만 패치
+ // Fetch only the data needed for the active tab.
const [files, commits] = await Promise.all([
tab === "files" ? getPullFiles(owner, repo, prNumber) : Promise.resolve(null),
tab === "commits" ? getPullCommits(owner, repo, prNumber) : Promise.resolve(null),
@@ -70,15 +70,15 @@ export default async function PullDetailPage({ params, searchParams }: Props) {
return (
<main className="flex-1 overflow-auto max-w-4xl mx-auto w-full px-6 py-6 space-y-5">
- {/* 뒤로가기 */}
+ {/* Back link. */}
<Link
href={`/${lang}/repository/${owner}/${repo}/pulls`}
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
>
← {dict.pulls.backToList}
</Link>
- {/* PR 헤더 */}
+ {/* PR header. */}
<div>
<div className="flex items-start gap-2 flex-wrap">
<PRBadge merged={!!pr.merged_at} state={pr.state} dict={dict.pulls} />
@@ -124,7 +124,7 @@ export default async function PullDetailPage({ params, searchParams }: Props) {
)}
</div>
- {/* 탭 네비게이션 */}
+ {/* Tab navigation. */}
<div className="flex gap-1 border-b border-border">
{tabs.map(({ key, label }) => (
<Link
@@ -142,7 +142,7 @@ export default async function PullDetailPage({ params, searchParams }: Props) {
))}
</div>
- {/* Overview */}
+ {/* Overview. */}
{tab === "overview" && (
<div className="border border-border rounded-lg p-4 text-foreground">
{pr.body?.trim() ? (
@@ -2,6 +2,7 @@
import { useParams } from "next/navigation";
+// Inline i18n strings here because error boundaries must be "use client" and cannot call getDictionary (server-only).
const messages = {
ko: { title: "데이터를 불러오지 못했습니다.", retry: "다시 시도" },
en: { title: "Failed to load data.", retry: "Try again" },
@@ -14,6 +14,7 @@ export default async function RepoLayout({ children, params }: Props) {
const { lang, owner, repo } = await params;
const locale = lang as Locale;
+ // Guard: reject any repo not in the allowlist before fetching or rendering anything.
const allowed = getAllowedRepos().some(
(r) => r.owner === owner && r.repo === repo
);
@@ -13,6 +13,7 @@ export default async function LangLayout({
return <>{children}</>;
}
+// Pre-render both locale roots at build time to avoid a dynamic fallback on first visit.
export function generateStaticParams() {
return [{ lang: "ko" }, { lang: "en" }];
}
@@ -4,14 +4,16 @@ import { getAllowedRepos } from "@/lib/github";
const BASE = "https://api.github.com";
const PAT = process.env.GITHUB_PAT!;
+// This route proxies all GitHub API requests so the PAT never reaches the client.
+// It validates every request against the GITHUB_REPOS allowlist before forwarding.
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
const { path } = await params;
const ghPath = path.join("/");
- // repos/{owner}/{repo}/... 형태에서 owner, repo 추출 후 허용 목록 검사
+ // Extract owner and repo from repos/{owner}/{repo}/... and validate against allowlist.
const match = ghPath.match(/^repos\/([^/]+)\/([^/]+)/);
if (match) {
const [, owner, repo] = match;
@@ -47,7 +47,7 @@
body { @apply bg-background text-foreground; }
}
-/* Shiki 다크모드 */
+/* Shiki dark mode. */
.dark .shiki,
.dark .shiki span {
color: var(--shiki-dark) !important;
@@ -13,7 +13,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
return (
<html lang="ko" suppressHydrationWarning>
<head>
- {/* 새로고침 시 다크모드 깜빡임 방지 */}
+ {/* Prevent dark mode flash on page load. */}
<script dangerouslySetInnerHTML={{
__html: `
try {
@@ -13,7 +13,7 @@ export async function CodeViewer({
}) {
const file = await getContents(owner, repo, path);
- // base64 디코딩
+ // Decode base64 content.
const raw = file.encoding === "base64"
? Buffer.from(file.content.replace(/\n/g, ""), "base64").toString("utf-8")
: file.content;
@@ -57,6 +57,7 @@ function DiffView({ patch }: { patch: string }) {
}
export function FilesChanged({ files, dict }: { files: GhPullFile[]; dict: Dict }) {
+ // Keyed by file SHA so that fold state survives list re-renders without index drift.
const [folded, setFolded] = useState<Record<string, boolean>>({});
const allFolded = files.every((f) => folded[f.sha]);
@@ -38,7 +38,7 @@ function buildTree(items: GhTreeItem[]): TreeNode[] {
}
}
- // 폴더 먼저, 파일 나중 정렬
+ // Sort: directories first, then files.
const sort = (nodes: TreeNode[]): TreeNode[] =>
nodes
.sort((a, b) => {
@@ -10,7 +10,7 @@ export function LangSwitcher({ currentLang }: { currentLang: Locale }) {
const pathname = usePathname();
function switchTo(lang: Locale) {
- // /ko/... → /en/... or /ko → /en
+ // Replace the locale segment: /ko/... → /en/... or /ko → /en.
const segments = pathname.split("/");
segments[1] = lang;
return segments.join("/") || "/";
@@ -37,12 +37,12 @@ export function RepoCard({
>
<div className="flex items-start justify-between gap-4">
<div className="min-w-0">
- {/* 레포 이름 */}
+ {/* Repo name. */}
<p className="font-semibold text-blue-600 dark:text-blue-400 truncate">
{name}
</p>
- {/* 설명 */}
+ {/* Description. */}
{repo.description ? (
<p className="text-sm text-muted-foreground mt-1 line-clamp-2">
{repo.description}
@@ -53,7 +53,7 @@ export function RepoCard({
</p>
)}
- {/* 토픽 태그 */}
+ {/* Topic tags. */}
{repo.topics.length > 0 && (
<div className="flex items-center gap-1.5 mt-2 flex-wrap">
{repo.topics.map((topic) => (
@@ -67,7 +67,7 @@ export function RepoCard({
</div>
)}
- {/* 메타 정보 */}
+ {/* Meta info. */}
<div className="flex items-center gap-4 mt-3 text-xs text-muted-foreground flex-wrap">
{langColor && repo.language && (
<span className="flex items-center gap-1.5">
@@ -90,7 +90,7 @@ export function RepoCard({
</div>
</div>
- {/* Private / Public 뱃지 */}
+ {/* Private/Public badge. */}
<span className="shrink-0 text-xs border border-border rounded-full px-2 py-0.5 text-muted-foreground">
{repo.private ? "Private" : "Public"}
</span>
@@ -2,13 +2,15 @@
import { useSyncExternalStore } from "react";
+// Watch for class changes on <html> so the icon stays in sync when the theme is toggled elsewhere.
function subscribe(cb: () => void) {
const mo = new MutationObserver(cb);
mo.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
return () => mo.disconnect();
}
export function ThemeToggle() {
+ // useSyncExternalStore provides the SSR-safe snapshot (() => false) to avoid hydration mismatch.
const dark = useSyncExternalStore(
subscribe,
() => document.documentElement.classList.contains("dark"),
@@ -25,7 +27,7 @@ export function ThemeToggle() {
<button
onClick={toggle}
className="p-2 rounded-md hover:bg-accent transition-colors text-muted-foreground hover:text-foreground"
- aria-label="테마 전환"
+ aria-label="Toggle theme"
suppressHydrationWarning
>
{dark ? (
@@ -7,6 +7,7 @@ const headers = {
"X-GitHub-Api-Version": "2022-11-28",
};
+// All GitHub API calls go through this helper so PAT headers and cache policy are applied consistently.
async function ghFetch<T>(path: string, params?: Record<string, string>): Promise<T> {
const url = new URL(`${BASE}/${path}`);
if (params) Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
@@ -15,37 +16,37 @@ async function ghFetch<T>(path: string, params?: Record<string, string>): Promis
return res.json();
}
-// 레포 정보
+// Repository info.
export async function getRepo(owner: string, repo: string) {
return ghFetch<GhRepo>(`repos/${owner}/${repo}`);
}
-// 허용된 레포 목록 (환경변수 기반)
+// Allowed repo list (from environment variables).
export function getAllowedRepos(): { owner: string; repo: string }[] {
const owner = process.env.GITHUB_OWNER!;
const repos = (process.env.GITHUB_REPOS ?? "").split(",").map((r) => r.trim()).filter(Boolean);
return repos.map((repo) => ({ owner, repo }));
}
-// 파일 트리
+// File tree.
export async function getTree(owner: string, repo: string, sha = "HEAD") {
return ghFetch<GhTree>(`repos/${owner}/${repo}/git/trees/${sha}`, { recursive: "1" });
}
-// 파일 내용
+// File contents.
export async function getContents(owner: string, repo: string, path: string) {
return ghFetch<GhContent>(`repos/${owner}/${repo}/contents/${encodeURIComponent(path)}`);
}
-// 커밋 목록
+// Commit list.
export async function getCommits(owner: string, repo: string, perPage = 30, page = 1) {
return ghFetch<GhCommit[]>(`repos/${owner}/${repo}/commits`, {
per_page: String(perPage),
page: String(page),
});
}
-// PR 목록
+// PR list.
export async function getPulls(owner: string, repo: string, state: "open" | "closed" | "all" = "all", perPage = 30, page = 1) {
return ghFetch<GhPull[]>(`repos/${owner}/${repo}/pulls`, {
state,
@@ -54,31 +55,31 @@ export async function getPulls(owner: string, repo: string, state: "open" | "clo
});
}
-// PR 단건
+// Single PR.
export async function getPull(owner: string, repo: string, number: number) {
return ghFetch<GhPull>(`repos/${owner}/${repo}/pulls/${number}`);
}
-// PR 변경 파일 목록
+// PR changed files.
export async function getPullFiles(owner: string, repo: string, number: number) {
return ghFetch<GhPullFile[]>(`repos/${owner}/${repo}/pulls/${number}/files`, {
per_page: "100",
});
}
-// PR 커밋 목록
+// PR commit list.
export async function getPullCommits(owner: string, repo: string, number: number) {
return ghFetch<GhCommit[]>(`repos/${owner}/${repo}/pulls/${number}/commits`, {
per_page: "100",
});
}
-// 커밋 단건 (파일 변경 포함)
+// Single commit (includes file changes).
export async function getCommit(owner: string, repo: string, sha: string) {
return ghFetch<GhCommitDetail>(`repos/${owner}/${repo}/commits/${sha}`);
}
-// --- 타입 ---
+// Types.
export type GhRepo = {
name: string;
full_name: string;
@@ -116,6 +117,7 @@ export type GhCommit = {
author: { login: string; avatar_url: string } | null;
};
+// GhPullFile is reused here because the GitHub API returns the same file-change shape for both PR files and commit files.
export type GhCommitDetail = GhCommit & {
stats: { additions: number; deletions: number; total: number };
files: GhPullFile[];
@@ -5,13 +5,13 @@ import { locales, defaultLocale, hasLocale } from "@/lib/i18n";
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
- // 이미 로케일 prefix가 있으면 통과
+ // Pass through if locale prefix already present.
const pathnameHasLocale = locales.some(
(l) => pathname === `/${l}` || pathname.startsWith(`/${l}/`)
);
if (pathnameHasLocale) return;
- // Accept-Language 헤더에서 로케일 감지
+ // Detect locale from Accept-Language header.
const acceptLang = request.headers.get("accept-language") ?? "";
const detected = acceptLang.split(",")[0].split("-")[0].toLowerCase();
const locale = hasLocale(detected) ? detected : defaultLocale;