Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions frontend/.claude/skills/design-reference/SKILL.md
Original file line number Diff line number Diff line change
@@ -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`로 대조.
1 change: 1 addition & 0 deletions frontend/.env.example
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
NEXT_PUBLIC_API_URL="http://localhost:3000"
SESSION_COOKIE_NAME="local-sid"
13 changes: 13 additions & 0 deletions frontend/src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Metadata } from "next";

export const metadata: Metadata = {
title: "대시보드",
};

export default function DashboardPage() {
return (
<div className="flex flex-1 items-center justify-center">
<p className="text-sm text-muted-foreground">대시보드 준비중입니다</p>
</div>
);
}
38 changes: 38 additions & 0 deletions frontend/src/app/login/_components/LoginPanel.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex w-85 flex-col gap-6">
<div className="flex flex-col gap-2">
<h1 className="text-2xl font-bold">로그인</h1>
<p className="text-sm text-muted-foreground">
오늘의 커밋에서 시작해 혼자서도 꾸준히 회고해요
</p>
</div>
<Button
size="lg"
className="w-full"
disabled={isRedirecting}
onClick={handleLogin}
>
<GithubIcon data-icon="inline-start" className="size-4" />
{isRedirecting ? "GitHub으로 이동하고 있어요" : "GitHub으로 로그인"}
</Button>
<span className="text-xs text-muted-foreground">
로그인하면 서비스 이용약관에 동의하게 돼요
</span>
</div>
);
}
23 changes: 23 additions & 0 deletions frontend/src/app/login/_components/MiniHeatmap.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="grid h-13 w-full auto-cols-fr grid-flow-col grid-rows-7 gap-0.5">
{Array.from({ length: WEEKS * DAYS }, (_, i) => {
const week = Math.floor(i / DAYS);
const day = i % DAYS;
const level = (week * 3 + day * 5) % 5;
return (
<span
key={i}
className="rounded-[2px]"
style={{ background: `var(${HEAT_VARS[level]})` }}
/>
);
})}
</div>
);
}
58 changes: 58 additions & 0 deletions frontend/src/app/login/_components/OnboardingPreview.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex flex-1 flex-col justify-between border-r border-[color:var(--border-subtle)] bg-card p-12">
<div className="flex items-center gap-2">
<div className="flex size-7 items-center justify-center rounded-lg bg-primary font-bold text-primary-foreground text-sm">
C
</div>
<span className="font-bold text-base">Commit Grow</span>
</div>

<div className="flex flex-col gap-4 rounded-xl border border-border bg-background p-6 shadow-[var(--shadow-floating)]">
<div className="grid grid-cols-3 gap-3">
{stats.map((stat) => (
<StatCard key={stat.label} {...stat} />
))}
</div>
<div className="h-px bg-[color:var(--border-subtle)]" />
<div className="flex flex-col gap-2">
<span className="text-xs text-muted-foreground">
회고 히트맵 · 최근 12주
</span>
<MiniHeatmap />
</div>
</div>

<div className="flex flex-col gap-3.5">
<div className="flex flex-col gap-2">
<h2 className="font-semibold text-lg">활동에서 시작하는 회고</h2>
<p className="max-w-[420px] text-muted-foreground text-sm">
GitHub 커밋과 PR을 바탕으로 질문을 받고, 답변은 Keep · Problem ·
Try로 자동 정리돼요
</p>
</div>
<div className="flex items-center gap-3.5">
<CaretLeftIcon className="size-4 text-[color:var(--text-disabled)]" />
<div className="flex items-center gap-1.5">
<span className="size-1.5 rounded-full bg-[color:var(--neutral-200)]" />
<span className="h-1.5 w-4 rounded-full bg-primary" />
<span className="size-1.5 rounded-full bg-[color:var(--neutral-200)]" />
</div>
<CaretRightIcon className="size-4 text-muted-foreground" />
</div>
</div>
</div>
);
}
17 changes: 17 additions & 0 deletions frontend/src/app/login/_components/StatCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
type StatCardProps = {
label: string;
value: string;
unit: string;
};

export function StatCard({ label, value, unit }: StatCardProps) {

Check warning on line 7 in frontend/src/app/login/_components/StatCard.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Mark the props of the component as read-only.

See more on https://sonarcloud.io/project/issues?id=f-lab-edu_commit-grow&issues=AZ_XYVC3dcOL3uW-m0lL&open=AZ_XYVC3dcOL3uW-m0lL&pullRequest=21
return (
<div className="flex flex-col gap-1">
<span className="text-xs text-muted-foreground">{label}</span>
<span className="flex items-baseline gap-0.5">
<span className="text-xl font-bold tabular-nums">{value}</span>
<span className="text-xs text-muted-foreground">{unit}</span>
</span>
</div>
);
}
19 changes: 19 additions & 0 deletions frontend/src/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex flex-1">
<OnboardingPreview />
<div className="flex flex-1 items-center justify-center p-16">
<LoginPanel />
</div>
</div>
);
}
18 changes: 18 additions & 0 deletions frontend/src/components/icons/CaretLeftIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { SVGProps } from "react";

export function CaretLeftIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
{...props}
>
<path d="M10 3L5 8l5 5" />
</svg>
);
}
18 changes: 18 additions & 0 deletions frontend/src/components/icons/CaretRightIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { SVGProps } from "react";

export function CaretRightIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
{...props}
>
<path d="M6 3l5 5-5 5" />
</svg>
);
}
9 changes: 9 additions & 0 deletions frontend/src/components/icons/GithubIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { SVGProps } from "react";

export function GithubIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" {...props}>
<path d="M8 0c-4.42 0-8 3.58-8 8a8.01 8.01 0 0 0 5.47 7.59c.4.08.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8Z" />
</svg>
);
}
17 changes: 14 additions & 3 deletions frontend/src/proxy.ts
Original file line number Diff line number Diff line change
@@ -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*"],
};
Loading