diff --git a/frontend/.claude/skills/design-reference/SKILL.md b/frontend/.claude/skills/design-reference/SKILL.md
new file mode 100644
index 0000000..7ff077b
--- /dev/null
+++ b/frontend/.claude/skills/design-reference/SKILL.md
@@ -0,0 +1,14 @@
+---
+name: design-reference
+description: 프론트엔드 UI 구현/수정 시 디자인 목업(Claude Design) 참고용. "디자인 목업", "목업 참고", "V1.dc.html", "피그마 대신" 등 화면 디자인을 확인해야 할 때 사용.
+---
+
+# 디자인 참고 (Claude Design MCP)
+
+디자인은 Figma 아님, Claude Design에 있음. 목업 내용 추측 금지 — 매번 MCP로 직접 읽고 구현.
+
+- project_id: `cb3076cd-70f8-4ff6-82a0-d45df794cbc8`
+- 참고 파일: `디자인 목업 V1.dc.html`
+- 토큰: `_ds/commit-grow-design-system-*/tokens/*.css`
+
+절차: (로그인 필요시 `/design-login`) → `read_design_skill`(hifi-design) → `read_file(project_id, "디자인 목업 V1.dc.html")` → 구현 → `render_preview`로 대조.
diff --git a/frontend/.env.example b/frontend/.env.example
index e7b7f81..3201b38 100644
--- a/frontend/.env.example
+++ b/frontend/.env.example
@@ -1 +1,2 @@
NEXT_PUBLIC_API_URL="http://localhost:3000"
+SESSION_COOKIE_NAME="local-sid"
diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx
new file mode 100644
index 0000000..78fee78
--- /dev/null
+++ b/frontend/src/app/dashboard/page.tsx
@@ -0,0 +1,13 @@
+import type { Metadata } from "next";
+
+export const metadata: Metadata = {
+ title: "대시보드",
+};
+
+export default function DashboardPage() {
+ return (
+
+ );
+}
diff --git a/frontend/src/app/login/_components/LoginPanel.tsx b/frontend/src/app/login/_components/LoginPanel.tsx
new file mode 100644
index 0000000..3cd754c
--- /dev/null
+++ b/frontend/src/app/login/_components/LoginPanel.tsx
@@ -0,0 +1,38 @@
+"use client";
+
+import { useState } from "react";
+
+import { GithubIcon } from "@/components/icons/GithubIcon";
+import { Button } from "@/components/ui/button";
+
+export function LoginPanel() {
+ const [isRedirecting, setIsRedirecting] = useState(false);
+
+ function handleLogin() {
+ setIsRedirecting(true);
+ window.location.href = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/auth/github`;
+ }
+
+ return (
+
+
+
로그인
+
+ 오늘의 커밋에서 시작해 혼자서도 꾸준히 회고해요
+
+
+
+
+ 로그인하면 서비스 이용약관에 동의하게 돼요
+
+
+ );
+}
diff --git a/frontend/src/app/login/_components/MiniHeatmap.tsx b/frontend/src/app/login/_components/MiniHeatmap.tsx
new file mode 100644
index 0000000..74413a4
--- /dev/null
+++ b/frontend/src/app/login/_components/MiniHeatmap.tsx
@@ -0,0 +1,23 @@
+const WEEKS = 12;
+const DAYS = 7;
+const HEAT_VARS = ["--heat-0", "--heat-1", "--heat-2", "--heat-3", "--heat-4"];
+
+// 로그인 화면 프리뷰용 정적 목업 데이터 — 실제 히트맵은 S-03 대시보드에서 구현
+export function MiniHeatmap() {
+ return (
+
+ {Array.from({ length: WEEKS * DAYS }, (_, i) => {
+ const week = Math.floor(i / DAYS);
+ const day = i % DAYS;
+ const level = (week * 3 + day * 5) % 5;
+ return (
+
+ );
+ })}
+
+ );
+}
diff --git a/frontend/src/app/login/_components/OnboardingPreview.tsx b/frontend/src/app/login/_components/OnboardingPreview.tsx
new file mode 100644
index 0000000..399be45
--- /dev/null
+++ b/frontend/src/app/login/_components/OnboardingPreview.tsx
@@ -0,0 +1,58 @@
+import { CaretLeftIcon } from "@/components/icons/CaretLeftIcon";
+import { CaretRightIcon } from "@/components/icons/CaretRightIcon";
+
+import { MiniHeatmap } from "./MiniHeatmap";
+import { StatCard } from "./StatCard";
+
+const stats = [
+ { label: "연속 회고", value: "7", unit: "일" },
+ { label: "전체 회고", value: "23", unit: "회" },
+ { label: "완료 액션포인트", value: "12", unit: "개" },
+];
+
+export function OnboardingPreview() {
+ return (
+
+
+
+
+
+ {stats.map((stat) => (
+
+ ))}
+
+
+
+
+ 회고 히트맵 · 최근 12주
+
+
+
+
+
+
+
+
활동에서 시작하는 회고
+
+ GitHub 커밋과 PR을 바탕으로 질문을 받고, 답변은 Keep · Problem ·
+ Try로 자동 정리돼요
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/app/login/_components/StatCard.tsx b/frontend/src/app/login/_components/StatCard.tsx
new file mode 100644
index 0000000..a46b8fb
--- /dev/null
+++ b/frontend/src/app/login/_components/StatCard.tsx
@@ -0,0 +1,17 @@
+type StatCardProps = {
+ label: string;
+ value: string;
+ unit: string;
+};
+
+export function StatCard({ label, value, unit }: StatCardProps) {
+ return (
+
+ {label}
+
+ {value}
+ {unit}
+
+
+ );
+}
diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx
new file mode 100644
index 0000000..7b6b877
--- /dev/null
+++ b/frontend/src/app/login/page.tsx
@@ -0,0 +1,19 @@
+import type { Metadata } from "next";
+
+import { LoginPanel } from "./_components/LoginPanel";
+import { OnboardingPreview } from "./_components/OnboardingPreview";
+
+export const metadata: Metadata = {
+ title: "로그인",
+};
+
+export default function LoginPage() {
+ return (
+
+ );
+}
diff --git a/frontend/src/components/icons/CaretLeftIcon.tsx b/frontend/src/components/icons/CaretLeftIcon.tsx
new file mode 100644
index 0000000..40b669a
--- /dev/null
+++ b/frontend/src/components/icons/CaretLeftIcon.tsx
@@ -0,0 +1,18 @@
+import type { SVGProps } from "react";
+
+export function CaretLeftIcon(props: SVGProps) {
+ return (
+
+ );
+}
diff --git a/frontend/src/components/icons/CaretRightIcon.tsx b/frontend/src/components/icons/CaretRightIcon.tsx
new file mode 100644
index 0000000..9b5fcf9
--- /dev/null
+++ b/frontend/src/components/icons/CaretRightIcon.tsx
@@ -0,0 +1,18 @@
+import type { SVGProps } from "react";
+
+export function CaretRightIcon(props: SVGProps) {
+ return (
+
+ );
+}
diff --git a/frontend/src/components/icons/GithubIcon.tsx b/frontend/src/components/icons/GithubIcon.tsx
new file mode 100644
index 0000000..4610a92
--- /dev/null
+++ b/frontend/src/components/icons/GithubIcon.tsx
@@ -0,0 +1,9 @@
+import type { SVGProps } from "react";
+
+export function GithubIcon(props: SVGProps) {
+ return (
+
+ );
+}
diff --git a/frontend/src/proxy.ts b/frontend/src/proxy.ts
index 8d55553..1becb88 100644
--- a/frontend/src/proxy.ts
+++ b/frontend/src/proxy.ts
@@ -1,10 +1,21 @@
import { type NextRequest, NextResponse } from "next/server";
-// 뼈대만 — 실제 인증 검증(세션 쿠키 확인 등)은 API 준비 후.
-export function proxy(_request: NextRequest) {
+// 세션 쿠키 존재 여부만 확인(값 유효성 검증 아님) — 백엔드 session.cookieName과 값 동기화 필요.
+const SESSION_COOKIE_NAME = process.env.SESSION_COOKIE_NAME ?? "local-sid";
+
+export function proxy(request: NextRequest) {
+ const hasSession = request.cookies.has(SESSION_COOKIE_NAME);
+ const { pathname } = request.nextUrl;
+
+ if (pathname === "/login" && hasSession) {
+ return NextResponse.redirect(new URL("/dashboard", request.url));
+ }
+ if (pathname.startsWith("/dashboard") && !hasSession) {
+ return NextResponse.redirect(new URL("/login", request.url));
+ }
return NextResponse.next();
}
export const config = {
- matcher: [],
+ matcher: ["/login", "/dashboard/:path*"],
};