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
9 changes: 9 additions & 0 deletions apps/timo-web/app/[locale]/(main)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import type { Metadata } from "next";

import { MainShellContainer } from "@/app/[locale]/(main)/_containers/MainShellContainer";
import { AuthGuardProvider } from "@/providers/auth/AuthGuardProvider";

export const metadata: Metadata = {
robots: {
index: false,
follow: false,
},
};

interface MainLayoutProps {
children: React.ReactNode;
}
Expand Down
58 changes: 54 additions & 4 deletions apps/timo-web/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import "./globals.css";
import localFont from "next/font/local";
import { notFound } from "next/navigation";
import { hasLocale, NextIntlClientProvider } from "next-intl";
import { setRequestLocale } from "next-intl/server";
import { getTranslations, setRequestLocale } from "next-intl/server";

import type { Metadata } from "next";

Expand All @@ -19,9 +19,11 @@ const pretendard = localFont({
display: "swap",
});

export const metadata: Metadata = {
title: "Timo",
description: "Timo web",
const SITE_URL = "https://timo.kr";

const OG_LOCALE: Record<(typeof routing.locales)[number], string> = {
en: "en_US",
ko: "ko_KR",
};

interface RootLayoutProps {
Expand All @@ -33,6 +35,54 @@ export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}

export async function generateMetadata({
params,
}: Omit<RootLayoutProps, "children">): Promise<Metadata> {
const { locale } = await params;

if (!hasLocale(routing.locales, locale)) {
notFound();
}

const t = await getTranslations({ locale, namespace: "Metadata" });
const title = t("title");
const description = t("description");

return {
metadataBase: new URL(SITE_URL),
title: {
default: title,
template: `%s | ${title}`,
},
description,
icons: {
icon: "/favicon.png",
},
openGraph: {
type: "website",
siteName: title,
title,
description,
locale: OG_LOCALE[locale],
alternateLocale: routing.locales
.filter((otherLocale) => otherLocale !== locale)
.map((otherLocale) => OG_LOCALE[otherLocale]),
},
twitter: {
card: "summary",
title,
description,
},
robots: {
index: true,
follow: true,
},
formatDetection: {
telephone: false,
},
};
}

export default async function RootLayout({
children,
params,
Expand Down
31 changes: 31 additions & 0 deletions apps/timo-web/app/[locale]/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,37 @@
import { notFound } from "next/navigation";
import { hasLocale } from "next-intl";
import { getTranslations } from "next-intl/server";

import type { Metadata } from "next";

import { LoginContainer } from "@/app/[locale]/login/_containers/LoginContainer";
import { routing } from "@/i18n/routing";
import { GuestGuardProvider } from "@/providers/auth/GuestGuardProvider";

interface LoginPageProps {
params: Promise<{ locale: string }>;
}

export async function generateMetadata({
params,
}: LoginPageProps): Promise<Metadata> {
const { locale } = await params;

if (!hasLocale(routing.locales, locale)) {
notFound();
}

const t = await getTranslations({ locale, namespace: "Login" });

return {
title: t("title"),
description: t("description"),
alternates: {
canonical: `/${locale}/login`,
},
};
}

export default function LoginPage() {
return (
<GuestGuardProvider>
Expand Down
31 changes: 31 additions & 0 deletions apps/timo-web/app/[locale]/policy/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,47 @@
import timoTextLogo from "@repo/timo-design-system/assets/images/logo/timo-text-logo.svg";
import Image from "next/image";
import { notFound } from "next/navigation";
import { hasLocale } from "next-intl";
import { getTranslations } from "next-intl/server";

import type { Metadata } from "next";

import { PolicyContainer } from "@/app/[locale]/policy/_containers/PolicyContainer";
import { AsyncBoundary } from "@/components/boundary/AsyncBoundary";
import { ROUTES } from "@/constants/routes";
import { Link } from "@/i18n/navigation";
import { routing } from "@/i18n/routing";
import { termsTypeSchema } from "@/types/terms-type";

interface PolicyPageProps {
params: Promise<{ locale: string }>;
searchParams: Promise<{ type?: string }>;
}

export async function generateMetadata({
params,
searchParams,
}: PolicyPageProps): Promise<Metadata> {
const { locale } = await params;

if (!hasLocale(routing.locales, locale)) {
notFound();
}

const { type } = await searchParams;
const parsedType = termsTypeSchema.safeParse(type);
const isPrivacy = parsedType.success && parsedType.data === "PRIVACY";

const t = await getTranslations({ locale, namespace: "Settings" });

return {
title: isPrivacy ? t("nav.privacy") : t("nav.policy"),
alternates: {
canonical: `/${locale}/policy`,
},
};
}

export default async function PolicyPage({ searchParams }: PolicyPageProps) {
const { type } = await searchParams;
const parsedType = termsTypeSchema.safeParse(type);
Expand Down
5 changes: 5 additions & 0 deletions apps/timo-web/messages/en.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{
"Metadata": {
"title": "timo",
"description": "Manage your day with clear timeboxes."
},
"Navigation": {
"home": "Home",
"today": "Today",
Expand Down Expand Up @@ -148,6 +152,7 @@
"timerStartFailed": "This to-do has no estimated duration set."
},
"Login": {
"title": "Log in",
"animationLabel": "Login animation",
"headline": "Manage your day with clear timeboxes.",
"description": "Please log in to your timo account.",
Expand Down
5 changes: 5 additions & 0 deletions apps/timo-web/messages/ko.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{
"Metadata": {
"title": "timo",
"description": "흩어진 시간을, 실행 가능한 흐름으로."
},
"Navigation": {
"home": "홈",
"today": "오늘",
Expand Down Expand Up @@ -148,6 +152,7 @@
"timerStartFailed": "예상 소요 시간이 설정되지 않은 투두입니다."
},
"Login": {
"title": "로그인",
"animationLabel": "로그인 애니메이션",
"headline": "흩어진 시간을, 실행 가능한 흐름으로.",
"description": "timo 계정에 로그인하세요.",
Expand Down
Binary file added apps/timo-web/public/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 23 additions & 11 deletions docs/architecture/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,35 @@ app/(domain)/page.tsx ← 라우팅·레이아웃 조
```

- **pages** (`app/.../page.tsx`): 라우팅 단위. 데이터 페칭(React Query) 및 레이아웃만 담당
- **도메인 종속** (`_components/`, `_containers/`, `_queries/`): 해당 도메인 라우트 내부에서만 사용. `_` 접두사로 Next.js 라우팅에서 제외됨
- **도메인 종속** (`_components/`, `_containers/`, `_hooks/`, `_queries/`, `_types/`, `_utils/`): 해당 도메인 라우트 내부에서만 사용. `_` 접두사로 Next.js 라우팅에서 제외됨
- `_components/` — props만 받는 순수 UI. `'use client'` 없이도 동작 가능
- `_containers/` — `'use client'` 필수. useQuery·zustand 등 외부 상태와 결합된 클라이언트 컨테이너
- `_hooks/` — 해당 도메인 전용 커스텀 훅 (React Query가 아닌 로컬 상태·이펙트 로직)
- `_queries/` — 해당 도메인 전용 React Query hooks (useQuery · useMutation · queryKey 정의)
- **앱 전역** (`components/`, `hooks/`): 여러 도메인에서 공유하는 presentational 컴포넌트·훅
- `_types/` — 해당 도메인 전용 타입
- `_utils/` — 해당 도메인 전용 순수 함수
- **앱 전역** (`components/`, `containers/`, `hooks/`, `queries/`): 여러 도메인에서 공유하는 presentational 컴포넌트·클라이언트 컨테이너·훅·쿼리
- **@repo/timo-design-system**: 앱 간 공유되는 범용 UI (Button, Card 등)

## 예시 — auth 도메인
## 예시 — focus 도메인

```text
app/
auth/
_components/ # LoginForm, SocialLoginButton 등 순수 UI
_containers/ # LoginFormContainer (useQuery·zustand 결합)
_queries/ # use-login-mutation, use-user-profile-query 등
page.tsx # 레이아웃 조합만 (Server Component)
layout.tsx
app/[locale]/(main)/focus/
_components/
FocusTaskItem.tsx # 순수 UI
FocusEmptyTaskItem.tsx
_containers/
FocusHeaderContainer.tsx # 'use client', useQuery·zustand 결합
FocusSessionContainer.tsx
_hooks/
use-focus-session.ts # 세션 타이머 등 로컬 로직
_queries/
use-focus-todo.ts # useQuery·useMutation 정의
_types/
task-type.ts
_utils/
date.ts
page.tsx # 레이아웃 조합만 (Server Component)
```

앱 전역 `queries/`는 여러 도메인에서 공유하는 쿼리(예: 유저 프로필)에 사용하고, 특정 도메인에만 쓰이는 쿼리는 `_queries/`로 해당 도메인에 코로케이션한다.
앱 전역 `queries/`는 여러 도메인에서 공유하는 쿼리(예: 유저 프로필 — `queries/use-my-profile.ts`)에 사용하고, 특정 도메인에만 쓰이는 쿼리는 `_queries/`로 해당 도메인에 코로케이션한다. 동일한 원칙이 컨테이너에도 적용되어, 여러 도메인이 공유하는 컨테이너는 앱 전역 `containers/`에 둔다.
32 changes: 16 additions & 16 deletions docs/architecture/stack.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
# 기술 스택

| 분류 | 도구 | 비고 |
| ------------ | -------------------------- | ----------------------------- |
| Monorepo | Turborepo + pnpm workspace | 빌드 캐싱, 파이프라인 최적화 |
| Framework | Next.js 16 (App Router) | SSR/SSG, Server Components |
| Language | TypeScript 5.9 | strict 모드 |
| Styling | Tailwind CSS | 유틸리티 클래스 기반 |
| State — 전역 | Zustand | 클라이언트 전역 상태 |
| State — 서버 | TanStack React Query | API 캐싱, 동기화 |
| API Client | swagger-typescript-api | OpenAPI 스펙 → 타입 자동 생성 |
| Monitoring | Sentry | 런타임 에러 및 성능 추적 |
| Formatter | Prettier | 팀 컨벤션 자동 적용 |
| Linter | ESLint 9 (flat config) | `--max-warnings 0` |
| Git Hooks | Husky | 커밋 전 lint/format |
| Code Review | CodeRabbit | PR마다 AI 리뷰 |
| Assets | SVG sprite | SVG → React 컴포넌트 |
| Deployment | Vercel | Turborepo Remote Cache 연동 |
| 분류 | 도구 | 비고 |
| ------------ | --------------------------------- | -------------------------------------------------- |
| Monorepo | Turborepo + pnpm workspace | 빌드 캐싱, 파이프라인 최적화 |
| Framework | Next.js 16 (App Router) | SSR/SSG, Server Components |
| Language | TypeScript 5.9 | strict 모드 |
| Styling | Tailwind CSS | 유틸리티 클래스 기반 |
| State — 전역 | Zustand | 클라이언트 전역 상태 |
| State — 서버 | TanStack React Query | API 캐싱, 동기화 |
| API Client | Orval (axios + react-query + zod) | OpenAPI 스펙 → React Query 훅·Zod 스키마 자동 생성 |
| Monitoring | Sentry | 런타임 에러 및 성능 추적 |
| Formatter | Prettier | 팀 컨벤션 자동 적용 |
| Linter | ESLint 9 (flat config) | `--max-warnings 0` |
| Git Hooks | Husky + lint-staged | 커밋 전 변경 파일만 lint/format |
| Code Review | CodeRabbit | PR마다 AI 리뷰 |
| Assets | SVGR | SVG → React 아이콘 컴포넌트 자동 생성 |
| Deployment | Vercel | Turborepo Remote Cache 연동 |
31 changes: 21 additions & 10 deletions docs/architecture/structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,31 @@ timo-client/
├── apps/
│ └── timo-web/ # Next.js 앱 (port 3000)
│ ├── app/ # App Router 페이지·레이아웃
│ │ └── (domain)/ # 도메인 라우트 (예: auth, onboarding)
│ │ ├── _components/ # 해당 도메인 전용 컴포넌트
│ │ └── [locale]/(그룹)/(domain)/ # 도메인 라우트 (예: home, focus, settings, onboarding)
│ │ ├── _components/ # 해당 도메인 전용 순수 UI
│ │ ├── _containers/ # 해당 도메인 전용 클라이언트 컨테이너 ('use client')
│ │ ├── _hooks/ # 해당 도메인 전용 커스텀 훅
│ │ ├── _queries/ # 해당 도메인 전용 React Query 훅
│ │ ├── _types/ # 해당 도메인 전용 타입
│ │ ├── _utils/ # 해당 도메인 전용 유틸
│ │ └── page.tsx
│ ├── components/ # 앱 전역 공유 컴포넌트
│ ├── hooks/ # 앱 전역 공유 커스텀 훅
│ ├── stores/ # Zustand 스토어
│ ├── queries/ # React Query 훅
│ └── lib/ # API 클라이언트, 유틸
│ ├── api/ # Orval 생성 API 클라이언트·타입 + 커스텀 axios 인스턴스
│ ├── components/ # 앱 전역 공유 컴포넌트
│ ├── constants/ # 앱 전역 상수 (라우트, 시간 단위 등)
│ ├── containers/ # 여러 도메인에서 공유하는 클라이언트 컨테이너
│ ├── hooks/ # 앱 전역 공유 커스텀 훅
│ ├── i18n/ # next-intl 라우팅·요청·네비게이션 설정
│ ├── messages/ # 로케일별 번역 리소스
│ ├── providers/ # 전역 Provider (auth, query, overlay, locale 등)
│ ├── queries/ # 여러 도메인에서 공유하는 React Query 훅
│ ├── stores/ # Zustand 스토어
│ ├── types/ # 앱 전역 공유 타입
│ └── utils/ # 앱 전역 공유 유틸
├── packages/
│ ├── timo-design-system/ # 공유 UI 컴포넌트 (@repo/timo-design-system)
│ ├── eslint-config/ # 공유 ESLint 설정 (@repo/eslint-config)
│ └── typescript-config/ # 공유 tsconfig (@repo/typescript-config)
│ ├── eslint-config/ # 공유 ESLint 설정 (@repo/eslint-config)
│ ├── tailwind-config/ # 공유 Tailwind 설정 (@repo/tailwind-config)
│ └── typescript-config/ # 공유 tsconfig (@repo/typescript-config)
└── docs/
```

Expand All @@ -27,4 +38,4 @@ timo-client/
- `apps/timo-web` → `packages/timo-design-system` 참조 가능
- `packages/timo-design-system` → `apps/*` 참조 금지
- 패키지 간 참조는 반드시 `workspace:*`로 선언
- 앱 내부 도메인 간 직접 import 금지 — 공유 로직은 `lib/` 또는 `packages/`로 추출
- 앱 내부 도메인 간 직접 import 금지 — 공유 로직은 앱 전역 디렉터리(`components/`, `hooks/`, `queries/`, `utils/`, `api/` 등) 또는 `packages/`로 추출
12 changes: 6 additions & 6 deletions docs/conventions/commit.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@

변경된 파일 위치에 따라 스코프를 결정한다.

| 스코프 | 기준 |
| -------- | ---------------------------------------------------------------------------- |
| `web` | `apps/timo-web/**` 변경 |
| `ui` | `packages/timo-design-system/**` 변경 |
| `root` | 루트 설정 파일 (`turbo.json`, `pnpm-workspace.yaml`, 루트 `package.json` 등) |
| `config` | `packages/eslint-config/**`, `packages/typescript-config/**` 변경 |
| 스코프 | 기준 |
| -------- | ------------------------------------------------------------------------------------------------ |
| `web` | `apps/timo-web/**` 변경 |
| `ui` | `packages/timo-design-system/**` 변경 |
| `root` | 루트 설정 파일 (`turbo.json`, `pnpm-workspace.yaml`, 루트 `package.json` 등) |
| `config` | `packages/eslint-config/**`, `packages/typescript-config/**`, `packages/tailwind-config/**` 변경 |

## 커밋 형식

Expand Down
Loading
Loading