From 8ca3b4ba4d282066fb58ed8091707e9efea6a445 Mon Sep 17 00:00:00 2001 From: JiHo Jeon Date: Fri, 3 Jul 2026 16:07:10 +0900 Subject: [PATCH 01/20] =?UTF-8?q?chore:=20Prettier=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B2=80=EC=82=AC=20=EC=8A=A4=ED=81=AC=EB=A6=BD?= =?UTF-8?q?=ED=8A=B8=20=EC=A0=81=EC=9A=A9=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .coderabbit.yaml | 14 ++--- .github/ISSUE_TEMPLATE/issue_template.md | 4 +- .prettierignore | 15 +++--- .prettierrc | 13 +++-- README.md | 66 ++++++++++++------------ docs/conventions/code-convention.md | 48 ++++++++--------- docs/conventions/prettier-convention.md | 20 +++---- eslint.config.mjs | 14 ++--- next.config.ts | 10 ++-- package.json | 6 ++- postcss.config.mjs | 2 +- src/app/globals.css | 15 +++--- src/app/layout.tsx | 28 +++++----- src/components/ui/button.tsx | 46 ++++++++--------- src/lib/client.ts | 2 +- src/lib/middleware.ts | 14 +++-- src/lib/server.ts | 4 +- src/lib/utils.ts | 4 +- 18 files changed, 168 insertions(+), 157 deletions(-) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index cc0ac3d..d48a743 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -1,26 +1,26 @@ # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json -language: "ko-KR" +language: 'ko-KR' reviews: - profile: "assertive" + profile: 'assertive' request_changes_workflow: false high_level_summary: true poem: false # 리뷰 제외 대상 (노이즈 방지) path_filters: - - "!node_modules/**" - - "!package-lock.json" - - "!.next/**" - - "!public/**" + - '!node_modules/**' + - '!package-lock.json' + - '!.next/**' + - '!public/**' # 프로젝트 내 커스텀 컨벤션 파일 학습 knowledge_base: code_guidelines: enabled: true filePatterns: - - "README.md" + - 'README.md' chat: auto_reply: true diff --git a/.github/ISSUE_TEMPLATE/issue_template.md b/.github/ISSUE_TEMPLATE/issue_template.md index d15fad8..6656a4c 100644 --- a/.github/ISSUE_TEMPLATE/issue_template.md +++ b/.github/ISSUE_TEMPLATE/issue_template.md @@ -28,13 +28,13 @@ -- [ ] +- [ ] ## 완료 기준 -- [ ] +- [ ] ## 참고 자료 diff --git a/.prettierignore b/.prettierignore index ae867a6..d00c30f 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,7 +1,8 @@ -node_modules -.next -out -build -coverage -public -package-lock.json \ No newline at end of file +node_modules +.next +out +build +coverage +public +package-lock.json +next-env.d.ts diff --git a/.prettierrc b/.prettierrc index c93c1a7..8792645 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,3 +1,10 @@ -{ - "plugins": ["prettier-plugin-tailwindcss"] -} +{ + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100, + "tabWidth": 2, + "arrowParens": "always", + "endOfLine": "lf", + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/README.md b/README.md index 18314d6..7a23080 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,11 @@ Slack · Notion · Jira 같은 기존 협업툴은 강력하지만, 협업툴에 처음부터 워크스페이스 구조를 직접 설계하지 않아도, 목적별 템플릿을 선택하면 공지·업무·일정·회의록·자료·채팅을 **한 공간에서** 바로 관리할 수 있습니다. -| 기존 협업툴 | Syncly | -| --- | --- | -| 사용자가 직접 구조를 설계 | 목적별 템플릿으로 자동 구성 | -| 기능이 많아 처음 쓰기 어려움 | 필요한 기능만 정리된 화면 | -| 여러 툴을 조합해야 함 | 한 공간에서 모든 협업 | +| 기존 협업툴 | Syncly | +| ---------------------------- | --------------------------- | +| 사용자가 직접 구조를 설계 | 목적별 템플릿으로 자동 구성 | +| 기능이 많아 처음 쓰기 어려움 | 필요한 기능만 정리된 화면 | +| 여러 툴을 조합해야 함 | 한 공간에서 모든 협업 | ## 타겟 사용자 @@ -23,34 +23,34 @@ Slack · Notion · Jira 같은 기존 협업툴은 강력하지만, 협업툴에 ## 주요 기능 (MVP) -| 기능 | 설명 | -| --- | --- | -| 로그인 | GitHub 소셜 로그인, 프로필 설정 | -| 템플릿 선택 | 팀플 / 프로젝트 / 매장 운영 템플릿 | -| 워크스페이스 생성 | 템플릿 기반 자동 구성, 생성자 = 관리자 | -| 프로젝트 관리 | 업무 생성, 담당자 지정, 상태(대기/진행중/완료), 마감일 | -| 캘린더 | 일정·마감일·회의 등록, 월간/주간 확인 | -| 공지 | 작성·수정·삭제, 중요 공지 고정 | -| 업무 회의록 | 회의 내용·참석자·결정사항·후속 업무 기록 | -| 자료실 | 파일 업로드, 링크 저장, 자료 목록 관리 | -| 채팅 | 워크스페이스 멤버 간 메시지 | -| 차트표 | 전체/완료 업무 개수, 진행률, 담당자별 통계 | -| 초대 | 초대 링크 생성, 멤버 목록 및 역할 관리 | +| 기능 | 설명 | +| ----------------- | ------------------------------------------------------ | +| 로그인 | GitHub 소셜 로그인, 프로필 설정 | +| 템플릿 선택 | 팀플 / 프로젝트 / 매장 운영 템플릿 | +| 워크스페이스 생성 | 템플릿 기반 자동 구성, 생성자 = 관리자 | +| 프로젝트 관리 | 업무 생성, 담당자 지정, 상태(대기/진행중/완료), 마감일 | +| 캘린더 | 일정·마감일·회의 등록, 월간/주간 확인 | +| 공지 | 작성·수정·삭제, 중요 공지 고정 | +| 업무 회의록 | 회의 내용·참석자·결정사항·후속 업무 기록 | +| 자료실 | 파일 업로드, 링크 저장, 자료 목록 관리 | +| 채팅 | 워크스페이스 멤버 간 메시지 | +| 차트표 | 전체/완료 업무 개수, 진행률, 담당자별 통계 | +| 초대 | 초대 링크 생성, 멤버 목록 및 역할 관리 | 자세한 내용은 PRD 문서를 참고하세요. ## 기술 스택 -| 영역 | 사용 기술 | -| --- | --- | -| 프레임워크 | [Next.js 16](https://nextjs.org) (App Router), React 19, TypeScript | -| 백엔드 / 인증 | [Supabase](https://supabase.com) (`@supabase/ssr`, `supabase-js`) | -| 서버 상태 | [TanStack Query](https://tanstack.com/query) | -| 클라이언트 상태 | [Zustand](https://zustand-demo.pmnd.rs) | -| UI | [Tailwind CSS v4](https://tailwindcss.com), [shadcn/ui](https://ui.shadcn.com), Radix UI, lucide-react | -| 폼 / 검증 | [react-hook-form](https://react-hook-form.com), [zod](https://zod.dev) | -| 레이아웃 | [react-grid-layout](https://github.com/react-grid-layout/react-grid-layout) (워크스페이스 컴포넌트 배치) | -| 날짜 | [date-fns](https://date-fns.org) | +| 영역 | 사용 기술 | +| --------------- | -------------------------------------------------------------------------------------------------------- | +| 프레임워크 | [Next.js 16](https://nextjs.org) (App Router), React 19, TypeScript | +| 백엔드 / 인증 | [Supabase](https://supabase.com) (`@supabase/ssr`, `supabase-js`) | +| 서버 상태 | [TanStack Query](https://tanstack.com/query) | +| 클라이언트 상태 | [Zustand](https://zustand-demo.pmnd.rs) | +| UI | [Tailwind CSS v4](https://tailwindcss.com), [shadcn/ui](https://ui.shadcn.com), Radix UI, lucide-react | +| 폼 / 검증 | [react-hook-form](https://react-hook-form.com), [zod](https://zod.dev) | +| 레이아웃 | [react-grid-layout](https://github.com/react-grid-layout/react-grid-layout) (워크스페이스 컴포넌트 배치) | +| 날짜 | [date-fns](https://date-fns.org) | ## 시작하기 @@ -79,12 +79,12 @@ npm run dev ## 스크립트 -| 명령어 | 설명 | -| --- | --- | -| `npm run dev` | 개발 서버 실행 | -| `npm run build` | 프로덕션 빌드 | +| 명령어 | 설명 | +| --------------- | ------------------ | +| `npm run dev` | 개발 서버 실행 | +| `npm run build` | 프로덕션 빌드 | | `npm run start` | 프로덕션 서버 실행 | -| `npm run lint` | ESLint 검사 | +| `npm run lint` | ESLint 검사 | ## 프로젝트 구조 diff --git a/docs/conventions/code-convention.md b/docs/conventions/code-convention.md index f4d22de..e5528e8 100644 --- a/docs/conventions/code-convention.md +++ b/docs/conventions/code-convention.md @@ -4,22 +4,22 @@ Syncly의 코드 구조, 네이밍, 품질 기준을 통일하기 위한 문서 ## 1. Naming Convention -| 대상 | 규칙 | 예시 | -| --- | --- | --- | -| 변수 | camelCase | `userList`, `activeIndex` | -| 함수 | camelCase, 동사로 시작 | `fetchUser`, `createWorkspace` | -| boolean 변수 | `is`, `has`, `can`, `should` 접두사 | `isOpen`, `hasPermission` | -| 상수 | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` | -| 객체/배열 상수 | camelCase | `moduleTypeMap` | -| 타입/인터페이스 | PascalCase | `WorkspaceModule`, `UserProfile` | -| React 컴포넌트 | PascalCase | `WorkspaceCard` | -| 커스텀 훅 | `use` 접두사 + camelCase | `useWorkspaceModules` | -| 이벤트 핸들러 | `handle` 접두사 | `handleSubmit` | -| 이벤트 핸들러 props | `on` 접두사 | `onSubmit`, `onClose` | -| 컴포넌트 파일 | PascalCase | `WorkspaceCard.tsx` | -| 유틸/훅/타입 파일 | kebab-case | `date-format.ts`, `use-auth.ts` | -| 폴더명 | kebab-case | `workspace-module` | -| URL 경로 | kebab-case | `/workspace-settings` | +| 대상 | 규칙 | 예시 | +| ------------------- | ----------------------------------- | -------------------------------- | +| 변수 | camelCase | `userList`, `activeIndex` | +| 함수 | camelCase, 동사로 시작 | `fetchUser`, `createWorkspace` | +| boolean 변수 | `is`, `has`, `can`, `should` 접두사 | `isOpen`, `hasPermission` | +| 상수 | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` | +| 객체/배열 상수 | camelCase | `moduleTypeMap` | +| 타입/인터페이스 | PascalCase | `WorkspaceModule`, `UserProfile` | +| React 컴포넌트 | PascalCase | `WorkspaceCard` | +| 커스텀 훅 | `use` 접두사 + camelCase | `useWorkspaceModules` | +| 이벤트 핸들러 | `handle` 접두사 | `handleSubmit` | +| 이벤트 핸들러 props | `on` 접두사 | `onSubmit`, `onClose` | +| 컴포넌트 파일 | PascalCase | `WorkspaceCard.tsx` | +| 유틸/훅/타입 파일 | kebab-case | `date-format.ts`, `use-auth.ts` | +| 폴더명 | kebab-case | `workspace-module` | +| URL 경로 | kebab-case | `/workspace-settings` | ## 2. TypeScript Convention @@ -166,14 +166,14 @@ PR 생성 전 아래 항목을 확인합니다. 아래 규칙은 팀 합의 후 ESLint를 통해 자동 검사할 예정입니다. -| 규칙 | 목적 | -| --- | --- | -| `console.log` 금지 | 디버깅 코드가 배포 코드에 포함되는 것을 방지 | -| unused import / variable 금지 | 불필요한 코드 제거 | -| `any` 사용 제한 | 타입 안정성 유지 | -| React Hooks 규칙 강제 | Hooks 오사용 방지 | -| FSD layer import 규칙 강제 | 프로젝트 구조 일관성 유지 | -| 공통 UI 중복 구현 제한 | UI 컴포넌트 재사용성 유지 | +| 규칙 | 목적 | +| ----------------------------- | -------------------------------------------- | +| `console.log` 금지 | 디버깅 코드가 배포 코드에 포함되는 것을 방지 | +| unused import / variable 금지 | 불필요한 코드 제거 | +| `any` 사용 제한 | 타입 안정성 유지 | +| React Hooks 규칙 강제 | Hooks 오사용 방지 | +| FSD layer import 규칙 강제 | 프로젝트 구조 일관성 유지 | +| 공통 UI 중복 구현 제한 | UI 컴포넌트 재사용성 유지 | ## 9. Design Token Rule diff --git a/docs/conventions/prettier-convention.md b/docs/conventions/prettier-convention.md index a33867b..3ae3f55 100644 --- a/docs/conventions/prettier-convention.md +++ b/docs/conventions/prettier-convention.md @@ -27,16 +27,16 @@ PR 전에는 반드시 포맷 검사를 통과해야 합니다. ## 3. 규칙 설명 -| 옵션 | 값 | 설명 | -| --- | --- | --- | -| `semi` | `true` | 문장 끝에 세미콜론을 사용합니다. | -| `singleQuote` | `true` | 문자열은 작은따옴표를 사용합니다. | -| `trailingComma` | `all` | 가능한 곳에는 trailing comma를 사용합니다. | -| `printWidth` | `100` | 한 줄 최대 길이는 100자로 제한합니다. | -| `tabWidth` | `2` | 들여쓰기는 2칸을 사용합니다. | -| `arrowParens` | `always` | 화살표 함수 인자는 항상 괄호를 사용합니다. | -| `endOfLine` | `lf` | 운영체제별 줄바꿈 차이를 방지하기 위해 LF로 통일합니다. | -| `plugins` | `prettier-plugin-tailwindcss` | Tailwind class를 자동 정렬합니다. | +| 옵션 | 값 | 설명 | +| --------------- | ----------------------------- | ------------------------------------------------------- | +| `semi` | `true` | 문장 끝에 세미콜론을 사용합니다. | +| `singleQuote` | `true` | 문자열은 작은따옴표를 사용합니다. | +| `trailingComma` | `all` | 가능한 곳에는 trailing comma를 사용합니다. | +| `printWidth` | `100` | 한 줄 최대 길이는 100자로 제한합니다. | +| `tabWidth` | `2` | 들여쓰기는 2칸을 사용합니다. | +| `arrowParens` | `always` | 화살표 함수 인자는 항상 괄호를 사용합니다. | +| `endOfLine` | `lf` | 운영체제별 줄바꿈 차이를 방지하기 위해 LF로 통일합니다. | +| `plugins` | `prettier-plugin-tailwindcss` | Tailwind class를 자동 정렬합니다. | ## 4. Line Ending diff --git a/eslint.config.mjs b/eslint.config.mjs index 05e726d..626ca82 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,6 +1,6 @@ -import { defineConfig, globalIgnores } from "eslint/config"; -import nextVitals from "eslint-config-next/core-web-vitals"; -import nextTs from "eslint-config-next/typescript"; +import { defineConfig, globalIgnores } from 'eslint/config'; +import nextVitals from 'eslint-config-next/core-web-vitals'; +import nextTs from 'eslint-config-next/typescript'; const eslintConfig = defineConfig([ ...nextVitals, @@ -8,10 +8,10 @@ const eslintConfig = defineConfig([ // Override default ignores of eslint-config-next. globalIgnores([ // Default ignores of eslint-config-next: - ".next/**", - "out/**", - "build/**", - "next-env.d.ts", + '.next/**', + 'out/**', + 'build/**', + 'next-env.d.ts', ]), ]); diff --git a/next.config.ts b/next.config.ts index 5e6a392..f6f93a2 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,4 +1,4 @@ -import type { NextConfig } from "next"; +import type { NextConfig } from 'next'; const nextConfig: NextConfig = { /* config options here */ @@ -6,12 +6,12 @@ const nextConfig: NextConfig = { images: { remotePatterns: [ { - protocol: "https", - hostname: "atkvaovvzdiznndrkjgt.supabase.co", + protocol: 'https', + hostname: 'atkvaovvzdiznndrkjgt.supabase.co', }, { - protocol: "http", - hostname: "localhost", + protocol: 'http', + hostname: 'localhost', }, ], }, diff --git a/package.json b/package.json index 29f6da5..8b58dd2 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,11 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "typecheck": "tsc --noEmit", + "format": "prettier --write .", + "format:check": "prettier --check .", + "check": "npm run lint && npm run typecheck && npm run format:check" }, "dependencies": { "@hookform/resolvers": "^5.4.0", diff --git a/postcss.config.mjs b/postcss.config.mjs index 61e3684..297374d 100644 --- a/postcss.config.mjs +++ b/postcss.config.mjs @@ -1,6 +1,6 @@ const config = { plugins: { - "@tailwindcss/postcss": {}, + '@tailwindcss/postcss': {}, }, }; diff --git a/src/app/globals.css b/src/app/globals.css index 94ce365..745cc7f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,8 +1,8 @@ -@import "tailwindcss"; -@import "tw-animate-css"; -@import "shadcn/tailwind.css"; -@import "react-grid-layout/css/styles.css"; -@import "react-resizable/css/styles.css"; +@import 'tailwindcss'; +@import 'tw-animate-css'; +@import 'shadcn/tailwind.css'; +@import 'react-grid-layout/css/styles.css'; +@import 'react-resizable/css/styles.css'; @custom-variant dark (&:is(.dark *)); @@ -126,10 +126,11 @@ body { @apply bg-background text-foreground; } - button:not(:disabled), [role="button"]:not(:disabled) { + button:not(:disabled), + [role='button']:not(:disabled) { cursor: pointer; } html { @apply font-sans; } -} \ No newline at end of file +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f4689aa..025e6a6 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,23 +1,23 @@ -import { cn } from "@/lib/utils"; -import type { Metadata } from "next"; -import { Geist, Geist_Mono, Inter } from "next/font/google"; -import "./globals.css"; +import { cn } from '@/lib/utils'; +import type { Metadata } from 'next'; +import { Geist, Geist_Mono, Inter } from 'next/font/google'; +import './globals.css'; -const inter = Inter({ subsets: ["latin"], variable: "--font-sans" }); +const inter = Inter({ subsets: ['latin'], variable: '--font-sans' }); const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], + variable: '--font-geist-sans', + subsets: ['latin'], }); const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], + variable: '--font-geist-mono', + subsets: ['latin'], }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: 'Create Next App', + description: 'Generated by create next app', }; export default function RootLayout({ @@ -29,11 +29,11 @@ export default function RootLayout({ diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 880376d..cecd6ac 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -1,57 +1,57 @@ -import * as React from "react"; -import { cva, type VariantProps } from "class-variance-authority"; -import { Slot } from "radix-ui"; +import * as React from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { Slot } from 'radix-ui'; -import { cn } from "@/lib/utils"; +import { cn } from '@/lib/utils'; const buttonVariants = cva( "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", { variants: { variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/80", + default: 'bg-primary text-primary-foreground hover:bg-primary/80', outline: - "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", + 'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50', secondary: - "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", + 'bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground', ghost: - "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50", + 'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50', destructive: - "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40", - link: "text-primary underline-offset-4 hover:underline", + 'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40', + link: 'text-primary underline-offset-4 hover:underline', }, size: { default: - "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", + 'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2', xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", - lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", - icon: "size-8", - "icon-xs": + lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2', + icon: 'size-8', + 'icon-xs': "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3", - "icon-sm": - "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg", - "icon-lg": "size-9", + 'icon-sm': + 'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg', + 'icon-lg': 'size-9', }, }, defaultVariants: { - variant: "default", - size: "default", + variant: 'default', + size: 'default', }, }, ); function Button({ className, - variant = "default", - size = "default", + variant = 'default', + size = 'default', asChild = false, ...props -}: React.ComponentProps<"button"> & +}: React.ComponentProps<'button'> & VariantProps & { asChild?: boolean; }) { - const Comp = asChild ? Slot.Root : "button"; + const Comp = asChild ? Slot.Root : 'button'; return ( - request.cookies.set(name, value), - ); + cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value)); supabaseResponse = NextResponse.next({ request, }); @@ -42,12 +40,12 @@ export async function updateSession(request: NextRequest) { if ( !user && - !request.nextUrl.pathname.startsWith("/login") && - !request.nextUrl.pathname.startsWith("/auth") + !request.nextUrl.pathname.startsWith('/login') && + !request.nextUrl.pathname.startsWith('/auth') ) { // no user, potentially respond by redirecting the user to the login page const url = request.nextUrl.clone(); - url.pathname = "/auth/login"; + url.pathname = '/auth/login'; return NextResponse.redirect(url); } diff --git a/src/lib/server.ts b/src/lib/server.ts index 12455a8..aa0d9d5 100644 --- a/src/lib/server.ts +++ b/src/lib/server.ts @@ -1,5 +1,5 @@ -import { createServerClient } from "@supabase/ssr"; -import { cookies } from "next/headers"; +import { createServerClient } from '@supabase/ssr'; +import { cookies } from 'next/headers'; /** * If using Fluid compute: Don't put this client in a global variable. Always create a new client within each diff --git a/src/lib/utils.ts b/src/lib/utils.ts index a5ef193..2819a83 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,5 +1,5 @@ -import { clsx, type ClassValue } from "clsx"; -import { twMerge } from "tailwind-merge"; +import { clsx, type ClassValue } from 'clsx'; +import { twMerge } from 'tailwind-merge'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); From 6812f8a4182dc5a5da7918d7e478fe65c25bfccf Mon Sep 17 00:00:00 2001 From: JiHo Jeon Date: Fri, 3 Jul 2026 16:07:10 +0900 Subject: [PATCH 02/20] =?UTF-8?q?chore:=20=EC=97=90=EC=9D=B4=EC=A0=84?= =?UTF-8?q?=ED=8A=B8=20=EA=B0=9C=EC=9D=B8=20=ED=8C=8C=EC=9D=BC=20gitignore?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 5ef6a52..aa4e491 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,7 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# agent files (개인 작업용) +AGENTS.md +.agents/ From 56d75ee5f625eb19565f56f9354efba4d210a9b1 Mon Sep 17 00:00:00 2001 From: JiHo Jeon Date: Fri, 3 Jul 2026 16:28:56 +0900 Subject: [PATCH 03/20] =?UTF-8?q?chore:=20=EC=95=A0=EB=8B=88=EB=A9=94?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=EB=9F=AC?= =?UTF-8?q?=EB=A6=AC=20motion=20=EC=B6=94=EA=B0=80=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 69 ++++++++++++++++++++++++ package.json | 1 + src/{ => shared}/lib/client.ts | 0 src/{ => shared}/lib/middleware.ts | 0 src/{ => shared}/lib/server.ts | 0 src/{ => shared}/lib/utils.ts | 0 src/{components => shared}/ui/button.tsx | 0 7 files changed, 70 insertions(+) rename src/{ => shared}/lib/client.ts (100%) rename src/{ => shared}/lib/middleware.ts (100%) rename src/{ => shared}/lib/server.ts (100%) rename src/{ => shared}/lib/utils.ts (100%) rename src/{components => shared}/ui/button.tsx (100%) diff --git a/package-lock.json b/package-lock.json index fe751bc..314852e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "clsx": "^2.1.1", "date-fns": "^4.4.0", "lucide-react": "^1.22.0", + "motion": "^12.42.2", "next": "16.2.9", "radix-ui": "^1.6.0", "react": "19.2.4", @@ -6743,6 +6744,33 @@ "node": ">= 0.6" } }, + "node_modules/framer-motion": { + "version": "12.42.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.2.tgz", + "integrity": "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.42.2", + "motion-utils": "^12.39.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", @@ -8539,6 +8567,47 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/motion": { + "version": "12.42.2", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.42.2.tgz", + "integrity": "sha512-Atvv11yUKIid41cVrRBDVX5m8tF8kNpExRSlbpt6APClhDjtwQssgFHhQzejxw7/7YYbjHSPKBVbHo05BuJT5Q==", + "license": "MIT", + "dependencies": { + "framer-motion": "^12.42.2", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "12.42.2", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.42.2.tgz", + "integrity": "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.39.0" + } + }, + "node_modules/motion-utils": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", + "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/package.json b/package.json index 8b58dd2..822019d 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "clsx": "^2.1.1", "date-fns": "^4.4.0", "lucide-react": "^1.22.0", + "motion": "^12.42.2", "next": "16.2.9", "radix-ui": "^1.6.0", "react": "19.2.4", diff --git a/src/lib/client.ts b/src/shared/lib/client.ts similarity index 100% rename from src/lib/client.ts rename to src/shared/lib/client.ts diff --git a/src/lib/middleware.ts b/src/shared/lib/middleware.ts similarity index 100% rename from src/lib/middleware.ts rename to src/shared/lib/middleware.ts diff --git a/src/lib/server.ts b/src/shared/lib/server.ts similarity index 100% rename from src/lib/server.ts rename to src/shared/lib/server.ts diff --git a/src/lib/utils.ts b/src/shared/lib/utils.ts similarity index 100% rename from src/lib/utils.ts rename to src/shared/lib/utils.ts diff --git a/src/components/ui/button.tsx b/src/shared/ui/button.tsx similarity index 100% rename from src/components/ui/button.tsx rename to src/shared/ui/button.tsx From 75062f76983e695ae1e9018ab65f28ef73ecc2c3 Mon Sep 17 00:00:00 2001 From: JiHo Jeon Date: Fri, 3 Jul 2026 16:28:57 +0900 Subject: [PATCH 04/20] =?UTF-8?q?refactor:=20FSD=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EB=8F=84=EC=9E=85=20=EB=B0=8F=20shared=20=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=96=B4=20=EC=9D=B4=EB=8F=99=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components.json | 10 +++++----- src/app/layout.tsx | 2 +- src/shared/ui/button.tsx | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/components.json b/components.json index 4f4eb5e..eb2988e 100644 --- a/components.json +++ b/components.json @@ -15,11 +15,11 @@ "menuColor": "default", "menuAccent": "subtle", "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" + "components": "@/shared/ui", + "utils": "@/shared/lib/utils", + "ui": "@/shared/ui", + "lib": "@/shared/lib", + "hooks": "@/shared/lib/hooks" }, "registries": { "@supabase": "https://supabase.com/ui/r/{name}.json" diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 025e6a6..bc58872 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,4 +1,4 @@ -import { cn } from '@/lib/utils'; +import { cn } from '@/shared/lib/utils'; import type { Metadata } from 'next'; import { Geist, Geist_Mono, Inter } from 'next/font/google'; import './globals.css'; diff --git a/src/shared/ui/button.tsx b/src/shared/ui/button.tsx index cecd6ac..6ee9a0b 100644 --- a/src/shared/ui/button.tsx +++ b/src/shared/ui/button.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { cva, type VariantProps } from 'class-variance-authority'; import { Slot } from 'radix-ui'; -import { cn } from '@/lib/utils'; +import { cn } from '@/shared/lib/utils'; const buttonVariants = cva( "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", From a18eafd77a39bf02e01441323b203b3e4a79fea9 Mon Sep 17 00:00:00 2001 From: JiHo Jeon Date: Fri, 3 Jul 2026 16:28:57 +0900 Subject: [PATCH 05/20] =?UTF-8?q?feat:=20=EB=B8=8C=EB=9E=9C=EB=93=9C=20?= =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=20=ED=86=A0=ED=81=B0=20=EB=B0=8F=20?= =?UTF-8?q?=EB=9E=9C=EB=94=A9=20views=20=EA=B3=A8=EA=B2=A9=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/globals.css | 11 +++++++++++ src/app/page.tsx | 9 ++++----- src/views/landing/index.ts | 2 ++ src/views/landing/ui/LandingPage.tsx | 9 +++++++++ 4 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 src/views/landing/index.ts create mode 100644 src/views/landing/ui/LandingPage.tsx diff --git a/src/app/globals.css b/src/app/globals.css index 745cc7f..e03869b 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -6,6 +6,17 @@ @custom-variant dark (&:is(.dark *)); +@theme { + --color-brand: #5b4ee8; + --color-brand-ink: #1a1b2e; + --color-brand-muted: #7b7fa8; + --color-brand-surface: #f7f8fc; + --color-brand-soft: #eef0fb; + --color-brand-start: #615fff; + --color-brand-end: #8e51ff; + --color-brand-deep: #7f22fe; +} + @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); diff --git a/src/app/page.tsx b/src/app/page.tsx index 6d736d0..036b276 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,7 +1,6 @@ +// 루트(/) 라우트 — views/landing의 페이지 컴포넌트를 렌더만 한다 +import { LandingPage } from '@/views/landing'; + export default function Home() { - return ( -
-
메인 페이지
-
- ); + return ; } diff --git a/src/views/landing/index.ts b/src/views/landing/index.ts new file mode 100644 index 0000000..c01e473 --- /dev/null +++ b/src/views/landing/index.ts @@ -0,0 +1,2 @@ +// landing 슬라이스의 Public API — 외부에서는 이 파일을 통해서만 import한다 +export { default as LandingPage } from './ui/LandingPage'; diff --git a/src/views/landing/ui/LandingPage.tsx b/src/views/landing/ui/LandingPage.tsx new file mode 100644 index 0000000..d8ae492 --- /dev/null +++ b/src/views/landing/ui/LandingPage.tsx @@ -0,0 +1,9 @@ +// 랜딩 페이지 — 랜딩 위젯(히어로, 팀 유형, 기능 소개, CTA)을 조립하는 페이지 컴포넌트 +export default function LandingPage() { + return ( +
+ {/* TODO(지호): 랜딩 위젯 섹션 조립 — hero, teams, features, cta */} +

Syncly

+
+ ); +} From ea9b94efb4f07170e8850721cca0ccd7468cb9ed Mon Sep 17 00:00:00 2001 From: JiHo Jeon Date: Fri, 3 Jul 2026 16:40:41 +0900 Subject: [PATCH 06/20] =?UTF-8?q?chore:=20ESLint=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=ED=92=88=EC=A7=88=20=EB=B0=8F=20FSD=20=EA=B2=BD=EA=B3=84=20?= =?UTF-8?q?=EA=B7=9C=EC=B9=99=20=EC=A0=81=EC=9A=A9=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eslint.config.mjs | 49 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/eslint.config.mjs b/eslint.config.mjs index 626ca82..d028ff8 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -2,9 +2,58 @@ import { defineConfig, globalIgnores } from 'eslint/config'; import nextVitals from 'eslint-config-next/core-web-vitals'; import nextTs from 'eslint-config-next/typescript'; +// FSD 레이어 경계 — 하위 레이어가 상위 레이어를 import하지 못하도록 제한한다. +// 순서: app → views → widgets → features → entities → shared +const fsdLayerRules = [ + { + layer: 'views', + forbidden: ['@/app/*'], + }, + { + layer: 'widgets', + forbidden: ['@/app/*', '@/views/*'], + }, + { + layer: 'features', + forbidden: ['@/app/*', '@/views/*', '@/widgets/*'], + }, + { + layer: 'entities', + forbidden: ['@/app/*', '@/views/*', '@/widgets/*', '@/features/*'], + }, + { + layer: 'shared', + forbidden: ['@/app/*', '@/views/*', '@/widgets/*', '@/features/*', '@/entities/*'], + }, +].map(({ layer, forbidden }) => ({ + files: [`src/${layer}/**/*.{ts,tsx}`], + rules: { + 'no-restricted-imports': [ + 'error', + { + patterns: [ + { + group: forbidden, + message: `FSD 규칙 위반: ${layer} 레이어는 상위 레이어를 import할 수 없습니다.`, + }, + ], + }, + ], + }, +})); + const eslintConfig = defineConfig([ ...nextVitals, ...nextTs, + { + rules: { + // 디버깅 코드가 배포 코드에 포함되는 것을 방지 (개발자 로그는 warn/error만 허용) + 'no-console': ['error', { allow: ['warn', 'error'] }], + // 타입 안정성 유지 + '@typescript-eslint/no-explicit-any': 'error', + }, + }, + ...fsdLayerRules, // Override default ignores of eslint-config-next. globalIgnores([ // Default ignores of eslint-config-next: From aeda616c40878a125bf2fc6c641fc4c389755c39 Mon Sep 17 00:00:00 2001 From: JiHo Jeon Date: Fri, 3 Jul 2026 16:40:42 +0900 Subject: [PATCH 07/20] =?UTF-8?q?design:=20=EB=B8=8C=EB=9E=9C=EB=93=9C=20?= =?UTF-8?q?=EB=B3=B4=EC=A1=B0=20=EB=B2=84=ED=8A=BC=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/globals.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/globals.css b/src/app/globals.css index e03869b..a899410 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -12,6 +12,7 @@ --color-brand-muted: #7b7fa8; --color-brand-surface: #f7f8fc; --color-brand-soft: #eef0fb; + --color-brand-secondary: #f1f3f9; --color-brand-start: #615fff; --color-brand-end: #8e51ff; --color-brand-deep: #7f22fe; From 26cc1a28694b4694c981f14aa467e6d7c2dee5a9 Mon Sep 17 00:00:00 2001 From: JiHo Jeon Date: Fri, 3 Jul 2026 16:40:42 +0900 Subject: [PATCH 08/20] =?UTF-8?q?feat:=20=EA=B3=B5=EC=9A=A9=20motion=20?= =?UTF-8?q?=EC=95=A0=EB=8B=88=EB=A9=94=EC=9D=B4=EC=85=98=20=EC=83=81?= =?UTF-8?q?=EC=88=98=20=EC=B6=94=EA=B0=80=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/lib/motion/index.ts | 10 +++++++++ src/shared/lib/motion/variants.ts | 36 +++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 src/shared/lib/motion/index.ts create mode 100644 src/shared/lib/motion/variants.ts diff --git a/src/shared/lib/motion/index.ts b/src/shared/lib/motion/index.ts new file mode 100644 index 0000000..12a6c49 --- /dev/null +++ b/src/shared/lib/motion/index.ts @@ -0,0 +1,10 @@ +// motion 상수 모듈의 Public API +export { + EASE_OUT, + DURATION_BASE, + STAGGER_CHILDREN, + VIEWPORT_ONCE, + fadeUp, + staggerContainer, + hoverLift, +} from './variants'; diff --git a/src/shared/lib/motion/variants.ts b/src/shared/lib/motion/variants.ts new file mode 100644 index 0000000..33f2504 --- /dev/null +++ b/src/shared/lib/motion/variants.ts @@ -0,0 +1,36 @@ +// Motion 공용 애니메이션 상수 — 랜딩 등에서 재사용하는 variants와 트랜지션 값을 중앙 관리한다 +import type { Variants } from 'motion/react'; + +// 부드러운 감속 곡선 (easeOutQuint 계열) — 등장 애니메이션 공통 이징 +export const EASE_OUT: [number, number, number, number] = [0.22, 1, 0.36, 1]; + +// 등장 애니메이션 기본 지속 시간(초) +export const DURATION_BASE = 0.6; + +// 스태거 등장 시 자식 간 시차(초) +export const STAGGER_CHILDREN = 0.12; + +// 스크롤 리빌 공통 뷰포트 옵션 — 한 번만 재생, 요소가 80px 들어왔을 때 시작 +export const VIEWPORT_ONCE = { once: true, margin: '-80px' } as const; + +// 아래에서 위로 떠오르며 나타나는 기본 등장 효과 +export const fadeUp: Variants = { + hidden: { opacity: 0, y: 24 }, + visible: { + opacity: 1, + y: 0, + transition: { duration: DURATION_BASE, ease: EASE_OUT }, + }, +}; + +// 자식들을 순차 등장시키는 컨테이너 — 자식에 fadeUp 등을 걸어 사용 +export const staggerContainer: Variants = { + hidden: {}, + visible: { transition: { staggerChildren: STAGGER_CHILDREN } }, +}; + +// 카드 호버 시 살짝 떠오르는 효과 — whileHover에 전달 +export const hoverLift = { + y: -6, + transition: { duration: 0.25, ease: EASE_OUT }, +} as const; From 3500e3adc4d151058e64687dc5ec175968181cac Mon Sep 17 00:00:00 2001 From: JiHo Jeon Date: Fri, 3 Jul 2026 16:50:31 +0900 Subject: [PATCH 09/20] =?UTF-8?q?feat:=20=EB=9E=9C=EB=94=A9=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=9C=84=EC=A0=AF=20=EB=B0=8F=20=EC=95=A0?= =?UTF-8?q?=EB=8B=88=EB=A9=94=EC=9D=B4=EC=85=98=20=EA=B5=AC=ED=98=84=20(#1?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/globals.css | 12 ++++ src/views/landing/ui/LandingPage.tsx | 29 ++++++-- src/widgets/landing-cta/index.ts | 3 + src/widgets/landing-cta/ui/CtaSection.tsx | 33 ++++++++++ src/widgets/landing-cta/ui/LandingFooter.tsx | 8 +++ .../landing-features/config/features.ts | 27 ++++++++ src/widgets/landing-features/index.ts | 2 + .../landing-features/ui/FeaturesSection.tsx | 41 ++++++++++++ src/widgets/landing-header/index.ts | 2 + .../landing-header/ui/LandingHeader.tsx | 58 ++++++++++++++++ src/widgets/landing-hero/index.ts | 2 + src/widgets/landing-hero/ui/HeroSection.tsx | 66 +++++++++++++++++++ src/widgets/landing-teams/config/teams.ts | 50 ++++++++++++++ src/widgets/landing-teams/index.ts | 2 + src/widgets/landing-teams/ui/TeamsSection.tsx | 55 ++++++++++++++++ 15 files changed, 385 insertions(+), 5 deletions(-) create mode 100644 src/widgets/landing-cta/index.ts create mode 100644 src/widgets/landing-cta/ui/CtaSection.tsx create mode 100644 src/widgets/landing-cta/ui/LandingFooter.tsx create mode 100644 src/widgets/landing-features/config/features.ts create mode 100644 src/widgets/landing-features/index.ts create mode 100644 src/widgets/landing-features/ui/FeaturesSection.tsx create mode 100644 src/widgets/landing-header/index.ts create mode 100644 src/widgets/landing-header/ui/LandingHeader.tsx create mode 100644 src/widgets/landing-hero/index.ts create mode 100644 src/widgets/landing-hero/ui/HeroSection.tsx create mode 100644 src/widgets/landing-teams/config/teams.ts create mode 100644 src/widgets/landing-teams/index.ts create mode 100644 src/widgets/landing-teams/ui/TeamsSection.tsx diff --git a/src/app/globals.css b/src/app/globals.css index a899410..a33a764 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -16,6 +16,18 @@ --color-brand-start: #615fff; --color-brand-end: #8e51ff; --color-brand-deep: #7f22fe; + + --animate-gradient-x: gradient-x 6s ease infinite; + + @keyframes gradient-x { + 0%, + 100% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + } } @theme inline { diff --git a/src/views/landing/ui/LandingPage.tsx b/src/views/landing/ui/LandingPage.tsx index d8ae492..0d58300 100644 --- a/src/views/landing/ui/LandingPage.tsx +++ b/src/views/landing/ui/LandingPage.tsx @@ -1,9 +1,28 @@ -// 랜딩 페이지 — 랜딩 위젯(히어로, 팀 유형, 기능 소개, CTA)을 조립하는 페이지 컴포넌트 +// 랜딩 페이지 — 랜딩 위젯(헤더, 히어로, 팀 유형, 기능 소개, CTA)을 조립하는 페이지 컴포넌트 +import { Plus_Jakarta_Sans } from 'next/font/google'; +import { CtaSection, LandingFooter } from '@/widgets/landing-cta'; +import { FeaturesSection } from '@/widgets/landing-features'; +import { LandingHeader } from '@/widgets/landing-header'; +import { HeroSection } from '@/widgets/landing-hero'; +import { TeamsSection } from '@/widgets/landing-teams'; + +// Figma 랜딩 디자인 지정 폰트 — 한글은 시스템 폰트로 fallback된다 +const jakarta = Plus_Jakarta_Sans({ + subsets: ['latin'], + weight: ['400', '600', '700', '800'], +}); + export default function LandingPage() { return ( -
- {/* TODO(지호): 랜딩 위젯 섹션 조립 — hero, teams, features, cta */} -

Syncly

-
+
+ +
+ + + + +
+ +
); } diff --git a/src/widgets/landing-cta/index.ts b/src/widgets/landing-cta/index.ts new file mode 100644 index 0000000..0a377f4 --- /dev/null +++ b/src/widgets/landing-cta/index.ts @@ -0,0 +1,3 @@ +// landing-cta 위젯의 Public API — CTA 섹션과 푸터를 함께 제공한다 +export { default as CtaSection } from './ui/CtaSection'; +export { default as LandingFooter } from './ui/LandingFooter'; diff --git a/src/widgets/landing-cta/ui/CtaSection.tsx b/src/widgets/landing-cta/ui/CtaSection.tsx new file mode 100644 index 0000000..fe67790 --- /dev/null +++ b/src/widgets/landing-cta/ui/CtaSection.tsx @@ -0,0 +1,33 @@ +'use client'; + +// 하단 CTA 섹션 — 보라 그라데이션 배경이 천천히 흐르고 버튼이 호버에 반응한다 +import { motion } from 'motion/react'; +import Link from 'next/link'; +import { fadeUp, staggerContainer, VIEWPORT_ONCE } from '@/shared/lib/motion'; + +export default function CtaSection() { + return ( + + + 지금 바로 무료로 시작하세요 + + + 신용카드 없이. 설치 없이. 복잡한 설정 없이. + + + + 무료로 시작하기 + + + + ); +} diff --git a/src/widgets/landing-cta/ui/LandingFooter.tsx b/src/widgets/landing-cta/ui/LandingFooter.tsx new file mode 100644 index 0000000..49d69ad --- /dev/null +++ b/src/widgets/landing-cta/ui/LandingFooter.tsx @@ -0,0 +1,8 @@ +// 랜딩 푸터 — 서비스 카피라이트를 표시하는 정적 영역 +export default function LandingFooter() { + return ( +
+

© 2026 Syncly. 소규모 팀을 위한 협업 플랫폼.

+
+ ); +} diff --git a/src/widgets/landing-features/config/features.ts b/src/widgets/landing-features/config/features.ts new file mode 100644 index 0000000..49467f1 --- /dev/null +++ b/src/widgets/landing-features/config/features.ts @@ -0,0 +1,27 @@ +// 기능 소개 카드 데이터 — PRD의 워크스페이스 구성 요소 7종 +import { + BarChart3, + Calendar, + ClipboardList, + FileText, + FolderOpen, + Megaphone, + MessageSquare, +} from 'lucide-react'; +import type { LucideIcon } from 'lucide-react'; + +export interface FeatureCard { + icon: LucideIcon; + title: string; + description: string; +} + +export const featureCards: FeatureCard[] = [ + { icon: ClipboardList, title: '업무 관리', description: '칸반 보드로 직관적인 업무 분배' }, + { icon: Calendar, title: '캘린더', description: '일정과 마감일을 한눈에' }, + { icon: Megaphone, title: '공지', description: '팀 공지를 놓치지 않게' }, + { icon: FileText, title: '회의록', description: '회의 내용을 간편하게 기록' }, + { icon: FolderOpen, title: '자료실', description: '파일과 링크를 한 곳에' }, + { icon: MessageSquare, title: '채팅', description: '팀원과 바로 소통' }, + { icon: BarChart3, title: '진행률 차트', description: '프로젝트 현황을 시각적으로' }, +]; diff --git a/src/widgets/landing-features/index.ts b/src/widgets/landing-features/index.ts new file mode 100644 index 0000000..6f28167 --- /dev/null +++ b/src/widgets/landing-features/index.ts @@ -0,0 +1,2 @@ +// landing-features 위젯의 Public API +export { default as FeaturesSection } from './ui/FeaturesSection'; diff --git a/src/widgets/landing-features/ui/FeaturesSection.tsx b/src/widgets/landing-features/ui/FeaturesSection.tsx new file mode 100644 index 0000000..91c4d78 --- /dev/null +++ b/src/widgets/landing-features/ui/FeaturesSection.tsx @@ -0,0 +1,41 @@ +'use client'; + +// 기능 소개 섹션 — 협업 기능 7종 카드가 스크롤 진입 시 그리드 순서대로 등장한다 +import { motion } from 'motion/react'; +import { fadeUp, staggerContainer, VIEWPORT_ONCE } from '@/shared/lib/motion'; +import { featureCards } from '../config/features'; + +export default function FeaturesSection() { + return ( +
+
+

+ 필요한 협업 기능만 딱 +

+ + {featureCards.map((feature) => ( + +
+ +
+

{feature.title}

+

+ {feature.description} +

+
+ ))} +
+
+
+ ); +} diff --git a/src/widgets/landing-header/index.ts b/src/widgets/landing-header/index.ts new file mode 100644 index 0000000..0f18205 --- /dev/null +++ b/src/widgets/landing-header/index.ts @@ -0,0 +1,2 @@ +// landing-header 위젯의 Public API +export { default as LandingHeader } from './ui/LandingHeader'; diff --git a/src/widgets/landing-header/ui/LandingHeader.tsx b/src/widgets/landing-header/ui/LandingHeader.tsx new file mode 100644 index 0000000..8eadca2 --- /dev/null +++ b/src/widgets/landing-header/ui/LandingHeader.tsx @@ -0,0 +1,58 @@ +'use client'; + +// 랜딩 상단 네비게이션 — 스크롤 시 배경 블러와 그림자가 나타나는 sticky 헤더 +import { useEffect, useState } from 'react'; +import Link from 'next/link'; +import { Boxes } from 'lucide-react'; +import { cn } from '@/shared/lib/utils'; + +const SCROLL_THRESHOLD = 8; + +export default function LandingHeader() { + const [isScrolled, setIsScrolled] = useState(false); + + useEffect(() => { + const handleScroll = () => { + setIsScrolled(window.scrollY > SCROLL_THRESHOLD); + }; + handleScroll(); + window.addEventListener('scroll', handleScroll, { passive: true }); + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, []); + + return ( +
+ +
+ ); +} diff --git a/src/widgets/landing-hero/index.ts b/src/widgets/landing-hero/index.ts new file mode 100644 index 0000000..3681040 --- /dev/null +++ b/src/widgets/landing-hero/index.ts @@ -0,0 +1,2 @@ +// landing-hero 위젯의 Public API +export { default as HeroSection } from './ui/HeroSection'; diff --git a/src/widgets/landing-hero/ui/HeroSection.tsx b/src/widgets/landing-hero/ui/HeroSection.tsx new file mode 100644 index 0000000..bd4adc7 --- /dev/null +++ b/src/widgets/landing-hero/ui/HeroSection.tsx @@ -0,0 +1,66 @@ +'use client'; + +// 랜딩 히어로 섹션 — 배지, 그라데이션 헤딩, CTA 버튼이 순차 등장한다 +import { motion } from 'motion/react'; +import Link from 'next/link'; +import { ArrowRight, Sparkles } from 'lucide-react'; +import { fadeUp, staggerContainer } from '@/shared/lib/motion'; + +// Figma 원본 버튼 그림자 값 — Tailwind 기본 shadow와 달라 임의값으로 유지한다 +const PRIMARY_BUTTON_SHADOW = '0px 10px 7.5px #c6d2ff, 0px 4px 3px #c6d2ff'; + +export default function HeroSection() { + return ( + + + + 소규모 팀을 위한 협업 플랫폼 + + + 상황에 맞는 워크스페이스를 +
+ + 바로 시작하세요 + +
+ + 팀플, 사이드 프로젝트, 소규모 매장 운영까지. +
+ 템플릿 하나면 필요한 협업 화면이 자동으로 구성됩니다. +
+ + + 지금 시작하기 + + + + 데모 보기 + + +
+ ); +} diff --git a/src/widgets/landing-teams/config/teams.ts b/src/widgets/landing-teams/config/teams.ts new file mode 100644 index 0000000..8e85225 --- /dev/null +++ b/src/widgets/landing-teams/config/teams.ts @@ -0,0 +1,50 @@ +// 팀 유형 카드 데이터 — 도메인 한정 포인트 컬러는 전역 토큰 대신 여기서 매핑 관리한다 +import { GraduationCap, Rocket, Store } from 'lucide-react'; +import type { LucideIcon } from 'lucide-react'; + +export interface TeamCard { + icon: LucideIcon; + title: string; + description: string; + gradient: string; + borderColor: string; + tagBg: string; + tagText: string; + tags: string[]; +} + +export const teamCards: TeamCard[] = [ + { + icon: GraduationCap, + title: '팀 프로젝트', + description: + '수업 팀플이나 스터디 그룹에 딱 맞는 구성입니다. 역할 분담부터 일정 관리까지 한 곳에서.', + gradient: 'linear-gradient(135deg, #8e51ff 0%, #615fff 100%)', + borderColor: '#ede9fe', + tagBg: '#ede9fe', + tagText: '#7008e7', + tags: ['업무 분담 보드', '회의록', '자료실', '캘린더', '그룹 채팅'], + }, + { + icon: Rocket, + title: '사이드 프로젝트', + description: + '빠르게 실행하는 소규모 개발팀을 위한 구성입니다. 칸반과 스프린트로 속도 있게 진행하세요.', + gradient: 'linear-gradient(135deg, #2b7fff 0%, #00b8db 100%)', + borderColor: '#dbeafe', + tagBg: '#dbeafe', + tagText: '#1447e6', + tags: ['칸반 보드', '스프린트 관리', '회의록', '채팅', '진행률 차트'], + }, + { + icon: Store, + title: '매장 운영', + description: + '매장 직원들과 공지, 스케줄, 업무를 쉽게 공유하세요. 복잡한 기능 없이 꼭 필요한 것만.', + gradient: 'linear-gradient(135deg, #fe9a00 0%, #ff6900 100%)', + borderColor: '#fef3c6', + tagBg: '#fef3c6', + tagText: '#bb4d00', + tags: ['공지 게시판', '업무 스케줄', '자료실', '채팅', '캘린더'], + }, +]; diff --git a/src/widgets/landing-teams/index.ts b/src/widgets/landing-teams/index.ts new file mode 100644 index 0000000..9f66a8d --- /dev/null +++ b/src/widgets/landing-teams/index.ts @@ -0,0 +1,2 @@ +// landing-teams 위젯의 Public API +export { default as TeamsSection } from './ui/TeamsSection'; diff --git a/src/widgets/landing-teams/ui/TeamsSection.tsx b/src/widgets/landing-teams/ui/TeamsSection.tsx new file mode 100644 index 0000000..340cd28 --- /dev/null +++ b/src/widgets/landing-teams/ui/TeamsSection.tsx @@ -0,0 +1,55 @@ +'use client'; + +// 팀 유형 소개 섹션 — 팀플/사이드 프로젝트/매장 운영 카드가 스크롤 진입 시 순차 등장한다 +import { motion } from 'motion/react'; +import { fadeUp, hoverLift, staggerContainer, VIEWPORT_ONCE } from '@/shared/lib/motion'; +import { teamCards } from '../config/teams'; + +export default function TeamsSection() { + return ( +
+
+

+ 어떤 팀인가요? +

+ + {teamCards.map((team) => ( + +
+ +
+

{team.title}

+

{team.description}

+
+ {team.tags.map((tag) => ( + + {tag} + + ))} +
+
+ ))} +
+
+
+ ); +} From d1cafb3983526ab25b5c794f6764c2f03ee2ee94 Mon Sep 17 00:00:00 2001 From: JiHo Jeon Date: Fri, 3 Jul 2026 16:51:00 +0900 Subject: [PATCH 10/20] =?UTF-8?q?chore:=20=EB=A1=9C=EC=BB=AC=20=EB=8F=84?= =?UTF-8?q?=EA=B5=AC=20=EC=84=A4=EC=A0=95=20gitignore=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index aa4e491..33f14f9 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ next-env.d.ts # agent files (개인 작업용) AGENTS.md .agents/ +.claude/ From 8a151325f44abbc30cd3a1dcc2cbf50ec06ba920 Mon Sep 17 00:00:00 2001 From: JiHo Jeon Date: Fri, 3 Jul 2026 17:07:08 +0900 Subject: [PATCH 11/20] =?UTF-8?q?rename:=20=EB=9E=9C=EB=94=A9=20=EC=9C=84?= =?UTF-8?q?=EC=A0=AF=EC=9D=84=20widgets/landing=20=EA=B7=B8=EB=A3=B9=20?= =?UTF-8?q?=ED=8F=B4=EB=8D=94=EB=A1=9C=20=EC=9D=B4=EB=8F=99=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/landing/ui/LandingPage.tsx | 10 +++++----- src/widgets/{ => landing}/landing-cta/index.ts | 0 .../{ => landing}/landing-cta/ui/CtaSection.tsx | 0 .../{ => landing}/landing-cta/ui/LandingFooter.tsx | 0 .../{ => landing}/landing-features/config/features.ts | 0 src/widgets/{ => landing}/landing-features/index.ts | 0 .../landing-features/ui/FeaturesSection.tsx | 0 src/widgets/{ => landing}/landing-header/index.ts | 0 .../{ => landing}/landing-header/ui/LandingHeader.tsx | 0 src/widgets/{ => landing}/landing-hero/index.ts | 0 .../{ => landing}/landing-hero/ui/HeroSection.tsx | 0 .../{ => landing}/landing-teams/config/teams.ts | 0 src/widgets/{ => landing}/landing-teams/index.ts | 0 .../{ => landing}/landing-teams/ui/TeamsSection.tsx | 0 14 files changed, 5 insertions(+), 5 deletions(-) rename src/widgets/{ => landing}/landing-cta/index.ts (100%) rename src/widgets/{ => landing}/landing-cta/ui/CtaSection.tsx (100%) rename src/widgets/{ => landing}/landing-cta/ui/LandingFooter.tsx (100%) rename src/widgets/{ => landing}/landing-features/config/features.ts (100%) rename src/widgets/{ => landing}/landing-features/index.ts (100%) rename src/widgets/{ => landing}/landing-features/ui/FeaturesSection.tsx (100%) rename src/widgets/{ => landing}/landing-header/index.ts (100%) rename src/widgets/{ => landing}/landing-header/ui/LandingHeader.tsx (100%) rename src/widgets/{ => landing}/landing-hero/index.ts (100%) rename src/widgets/{ => landing}/landing-hero/ui/HeroSection.tsx (100%) rename src/widgets/{ => landing}/landing-teams/config/teams.ts (100%) rename src/widgets/{ => landing}/landing-teams/index.ts (100%) rename src/widgets/{ => landing}/landing-teams/ui/TeamsSection.tsx (100%) diff --git a/src/views/landing/ui/LandingPage.tsx b/src/views/landing/ui/LandingPage.tsx index 0d58300..3772508 100644 --- a/src/views/landing/ui/LandingPage.tsx +++ b/src/views/landing/ui/LandingPage.tsx @@ -1,10 +1,10 @@ // 랜딩 페이지 — 랜딩 위젯(헤더, 히어로, 팀 유형, 기능 소개, CTA)을 조립하는 페이지 컴포넌트 import { Plus_Jakarta_Sans } from 'next/font/google'; -import { CtaSection, LandingFooter } from '@/widgets/landing-cta'; -import { FeaturesSection } from '@/widgets/landing-features'; -import { LandingHeader } from '@/widgets/landing-header'; -import { HeroSection } from '@/widgets/landing-hero'; -import { TeamsSection } from '@/widgets/landing-teams'; +import { CtaSection, LandingFooter } from '@/widgets/landing/landing-cta'; +import { FeaturesSection } from '@/widgets/landing/landing-features'; +import { LandingHeader } from '@/widgets/landing/landing-header'; +import { HeroSection } from '@/widgets/landing/landing-hero'; +import { TeamsSection } from '@/widgets/landing/landing-teams'; // Figma 랜딩 디자인 지정 폰트 — 한글은 시스템 폰트로 fallback된다 const jakarta = Plus_Jakarta_Sans({ diff --git a/src/widgets/landing-cta/index.ts b/src/widgets/landing/landing-cta/index.ts similarity index 100% rename from src/widgets/landing-cta/index.ts rename to src/widgets/landing/landing-cta/index.ts diff --git a/src/widgets/landing-cta/ui/CtaSection.tsx b/src/widgets/landing/landing-cta/ui/CtaSection.tsx similarity index 100% rename from src/widgets/landing-cta/ui/CtaSection.tsx rename to src/widgets/landing/landing-cta/ui/CtaSection.tsx diff --git a/src/widgets/landing-cta/ui/LandingFooter.tsx b/src/widgets/landing/landing-cta/ui/LandingFooter.tsx similarity index 100% rename from src/widgets/landing-cta/ui/LandingFooter.tsx rename to src/widgets/landing/landing-cta/ui/LandingFooter.tsx diff --git a/src/widgets/landing-features/config/features.ts b/src/widgets/landing/landing-features/config/features.ts similarity index 100% rename from src/widgets/landing-features/config/features.ts rename to src/widgets/landing/landing-features/config/features.ts diff --git a/src/widgets/landing-features/index.ts b/src/widgets/landing/landing-features/index.ts similarity index 100% rename from src/widgets/landing-features/index.ts rename to src/widgets/landing/landing-features/index.ts diff --git a/src/widgets/landing-features/ui/FeaturesSection.tsx b/src/widgets/landing/landing-features/ui/FeaturesSection.tsx similarity index 100% rename from src/widgets/landing-features/ui/FeaturesSection.tsx rename to src/widgets/landing/landing-features/ui/FeaturesSection.tsx diff --git a/src/widgets/landing-header/index.ts b/src/widgets/landing/landing-header/index.ts similarity index 100% rename from src/widgets/landing-header/index.ts rename to src/widgets/landing/landing-header/index.ts diff --git a/src/widgets/landing-header/ui/LandingHeader.tsx b/src/widgets/landing/landing-header/ui/LandingHeader.tsx similarity index 100% rename from src/widgets/landing-header/ui/LandingHeader.tsx rename to src/widgets/landing/landing-header/ui/LandingHeader.tsx diff --git a/src/widgets/landing-hero/index.ts b/src/widgets/landing/landing-hero/index.ts similarity index 100% rename from src/widgets/landing-hero/index.ts rename to src/widgets/landing/landing-hero/index.ts diff --git a/src/widgets/landing-hero/ui/HeroSection.tsx b/src/widgets/landing/landing-hero/ui/HeroSection.tsx similarity index 100% rename from src/widgets/landing-hero/ui/HeroSection.tsx rename to src/widgets/landing/landing-hero/ui/HeroSection.tsx diff --git a/src/widgets/landing-teams/config/teams.ts b/src/widgets/landing/landing-teams/config/teams.ts similarity index 100% rename from src/widgets/landing-teams/config/teams.ts rename to src/widgets/landing/landing-teams/config/teams.ts diff --git a/src/widgets/landing-teams/index.ts b/src/widgets/landing/landing-teams/index.ts similarity index 100% rename from src/widgets/landing-teams/index.ts rename to src/widgets/landing/landing-teams/index.ts diff --git a/src/widgets/landing-teams/ui/TeamsSection.tsx b/src/widgets/landing/landing-teams/ui/TeamsSection.tsx similarity index 100% rename from src/widgets/landing-teams/ui/TeamsSection.tsx rename to src/widgets/landing/landing-teams/ui/TeamsSection.tsx From f42153b03e81eb14cb0b09c468b29105521af191 Mon Sep 17 00:00:00 2001 From: JiHo Jeon Date: Fri, 3 Jul 2026 17:07:08 +0900 Subject: [PATCH 12/20] =?UTF-8?q?design:=20Tailwind=20=ED=94=84=EB=A6=AC?= =?UTF-8?q?=EC=85=8B=20=EC=B9=98=ED=99=98=20=EB=B0=8F=20=EB=AA=A8=EB=B0=94?= =?UTF-8?q?=EC=9D=BC=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20Figma=20?= =?UTF-8?q?=EC=A0=95=ED=95=A9=20=EC=88=98=EC=A0=95=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/widgets/landing/landing-cta/ui/CtaSection.tsx | 2 +- src/widgets/landing/landing-cta/ui/LandingFooter.tsx | 2 +- .../landing/landing-features/ui/FeaturesSection.tsx | 2 +- .../landing/landing-header/ui/LandingHeader.tsx | 2 +- src/widgets/landing/landing-hero/ui/HeroSection.tsx | 10 +++++----- src/widgets/landing/landing-teams/ui/TeamsSection.tsx | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/widgets/landing/landing-cta/ui/CtaSection.tsx b/src/widgets/landing/landing-cta/ui/CtaSection.tsx index fe67790..9bda42a 100644 --- a/src/widgets/landing/landing-cta/ui/CtaSection.tsx +++ b/src/widgets/landing/landing-cta/ui/CtaSection.tsx @@ -23,7 +23,7 @@ export default function CtaSection() { 무료로 시작하기 diff --git a/src/widgets/landing/landing-cta/ui/LandingFooter.tsx b/src/widgets/landing/landing-cta/ui/LandingFooter.tsx index 49d69ad..512025c 100644 --- a/src/widgets/landing/landing-cta/ui/LandingFooter.tsx +++ b/src/widgets/landing/landing-cta/ui/LandingFooter.tsx @@ -1,7 +1,7 @@ // 랜딩 푸터 — 서비스 카피라이트를 표시하는 정적 영역 export default function LandingFooter() { return ( -