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
173 changes: 173 additions & 0 deletions .agents/skills/frontend-architecture-guardrails/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
---
name: frontend-architecture-guardrails
description: Use when Codex designs, implements, reviews, or refactors frontend code, especially React, Next.js App Router, TanStack Router, TanStack Start, Next-to-TanStack migrations, and OC-ADMIN work, to keep responsibility ownership, cohesive feature/domain structure, route/page and server/client boundaries, UI-to-state mapping, loading/error/Suspense behavior, abstraction boundaries, provider scope, API hooks, utilities, and tests clean.
---

# Frontend Architecture Guardrails

Apply these checks before and during frontend coding. Keep the code pragmatic: cohesive, easy to move or delete, and not over-abstracted.

## First Pass

Before editing, identify:

- Feature boundary: which product capability owns the change.
- State owner: route, page, feature hook, React Query, local component, or context.
- Data flow: API response -> mapper/type -> query hook -> page state/derived data -> UI.
- Side effects: navigation, mutation, cache invalidation, storage, timers, subscriptions.
- UI ownership: which component owns layout, which owns interaction, which only renders.

If any owner is unclear, inspect nearby code first and choose the smallest owner that can naturally contain the behavior.

## Folder Ownership

Prefer feature/domain cohesion over technical buckets for product code.

Recommended shape:

```txt
src/
routes/ route declarations only
features/ feature-owned pages, components, hooks, api, types, utils
shared/ stable cross-feature components, hooks, utils, types
```

Route files should stay thin:

- Do: `createFileRoute`, `beforeLoad`, `loader`, `validateSearch`, params/search parsing, redirects, route-level error/pending components.
- Do not: put large JSX, form state, mutation logic, domain calculations, or feature-only UI helpers in routes.

Feature folders own feature-specific code:

- `features/auth/login/LoginPage.tsx`
- `features/dashboard/list`, `features/dashboard/detail`, `features/dashboard/create`
- `features/review/list`, `features/review/detail`

Use `shared/` only when the code is used by multiple features or is intentionally stable. Do not use `shared/` as a parking lot.

## Responsibility Rules

Separate responsibilities by what changes together:

- Page component: compose feature sections and page-scoped providers.
- Section/container component: arrange UI and own local coordination.
- Leaf component: render one UI concept with narrow props.
- Hook: own interaction state, side effects, query/mutation orchestration, or event translation.
- API module: own endpoint calls only.
- Mapper/adapter: own API-to-view/domain shape conversion.
- Utility: pure calculation, filtering, formatting, parsing, or selection.

Avoid components that fetch, transform, navigate, mutate, and render all in one file.

## Abstraction Rules

Extract only when there is a real reason:

- A name captures a product/domain concept.
- A boundary isolates side effects or API details.
- A pure function becomes testable.
- Repetition has the same reason to change, not just similar syntax.
- A component interface becomes smaller or less coupled.

Do not create generic abstractions just because two blocks look similar. Prefer duplication over a weak abstraction that hides intent.

## State And Providers

Keep provider scope as small as possible:

- App root: QueryClient, router shell, global styles, global devtools, true app-wide auth/session shell.
- Feature page: feature-only contexts, wizard state, selected tab/product/item state shared by several child components.
- Local component: input display state, modal open state, hover/expanded state.

Use React Query for server state. Do not mirror server data into Context unless there is a concrete reason.

For derived data, prefer `useMemo` near the component that needs it, or a pure utility if the rule is domain logic.

## UI 1:1 Mapping

Make UI boundaries follow what the user sees:

- One visible section usually maps to one section component.
- A repeated row/card maps to a leaf component.
- Form display formatting is separate from the persisted domain value.
- Event handlers should accept domain-ish payloads when possible, not raw DOM events deep in hooks.

Example: convert `onChange` event to `{ field, value }` near the UI boundary, then let hooks work with those values.

## TanStack Router Defaults

For TanStack Router or TanStack Start:

- Keep `src/routes/*` as address wiring.
- Use `beforeLoad` for auth/permission redirects before rendering.
- Use `loader` for route-owned prefetchable data.
- Use `validateSearch` for typed query-string state.
- Use `$id` route params and `Route.useParams()` instead of ad hoc URL parsing.
- Keep `routeTree.gen.ts` generated and never edit it manually.

For OC-ADMIN migration, prefer:

```txt
routes/login.tsx -> imports feature page
features/auth/login/LoginPage.tsx
features/auth/login/hooks/
features/auth/api/
features/dashboard/...
shared/components/
shared/utils/
```

Keep aliases consistent within a migration. Prefer the scaffold alias (`#/*`) unless the repo has already standardized another alias.

## API And Query Layer

Keep endpoint calls boring and typed:

- API function: one endpoint or one cohesive backend action.
- Query hook: owns query key, query function, cache options, and status interface.
- Mutation hook: owns mutation call, invalidation, optimistic behavior if any, and error normalization.
- Mapper: converts backend shape to UI/domain shape when the API shape is noisy or unstable.

Do not let UI components know raw endpoint paths, response envelope quirks, or repeated cache invalidation details.

For detailed guidance on React Query loading/error handling, business errors,
Suspense placement, and Next.js Server/Client Component boundaries, read
`references/front-tip.md` when a task touches those choices.

## Testable Logic

Move these into pure utilities and test when behavior matters:

- filtering, sorting, selection
- number/date formatting and parsing
- API response mapping
- validation rules
- derived status labels
- permission/visibility rules

Prioritize tests for utilities with branching, reused mappers, and logic that could silently regress.

## Red Flags

Pause and restructure when you see:

- A route file with real page implementation.
- A global provider created for one page.
- A component that owns API calls, transformations, mutations, navigation, and layout.
- `shared/` receiving feature-specific code.
- Props named after implementation details like `filteredItems` when the child only needs `items`.
- Type assertions replacing validation or type guards.
- Hooks coupled to a specific UI library's event object without need.
- Copy-pasted loading/error logic across many pages.

## Working Loop

For each frontend change:

1. Inspect nearby structure and name the owner.
2. Place files by ownership, not by habit.
3. Keep route files thin and feature files cohesive.
4. Extract pure logic before it grows inside JSX.
5. Scope providers and state narrowly.
6. Run the smallest meaningful verification: typecheck/build/test or a focused route smoke check.
7. In the final response, mention any ownership or structure choice only if it affects future work.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
interface:
display_name: "Frontend Architecture Guardrails"
short_description: "Frontend structure checks"
default_prompt: "Use $frontend-architecture-guardrails to design, implement, review, or refactor frontend code with clear ownership, cohesive feature structure, route/server-client boundaries, loading and error behavior, Suspense placement, abstractions, providers, and tests."

policy:
allow_implicit_invocation: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Frontend Tip Reference

Use this reference when a frontend task touches Next.js App Router
Server/Client Component boundaries, React Query loading/error handling,
business error modeling, or Suspense placement.

This file distills the user's `front-tip` notes into guardrails for code
generation, review, and refactoring.

## Next.js App Router Boundaries

- Treat `app/page.tsx` and `app/layout.tsx` as Server Components by default.
- Keep `metadata` and `generateMetadata` in Server Components.
- Do not add `"use client"` to a route/page file just to support one
interactive child.
- Push `"use client"` as far down as practical, near the component that needs
browser state, events, React Query hooks, or browser APIs.
- When a page needs mostly client behavior but also route metadata, keep a
server wrapper page and render a client page/component inside it.
- React Query hooks run in Client Components. If server-side first paint matters,
prefer server prefetch plus hydration instead of turning the whole page into a
Client Component.

Preferred shape:

```tsx
// page.tsx - Server Component
export const metadata = {
title: "Posts",
};

export default function Page() {
return <PostsClientPage />;
}
```

```tsx
// PostsClientPage.tsx
"use client";

export default function PostsClientPage() {
return <div />;
}
```

## React Query Loading And Error Handling

Use `useQuery` status flags when the screen is small, local, or needs precise
pending/error/refetching behavior:

```tsx
const { data, isPending, isError, error } = useQuery(options);

if (isPending) return <ListLoading />;
if (isError) return <ErrorMessage error={error} />;

return <List items={data} />;
```

Use `useSuspenseQuery` with `Suspense` and an `ErrorBoundary` when a page or
section should centralize loading/error behavior and the child should render
only the success state:

```tsx
<ErrorBoundary>
<Suspense fallback={<SectionSkeleton />}>
<Section />
</Suspense>
</ErrorBoundary>
```

Do not use Suspense only to avoid checking `data | undefined`. Choose it when
the UI boundary, loading sequence, and error ownership are clearer.

## Business Errors Vs Exceptions

Do not model every failure as `throw new Error()`.

Use thrown errors or rejected promises for technical failures:

- network/server failure
- broken API response shape
- missing required infrastructure data
- unexpected runtime errors

Model expected business failures as explicit return state when the UI should
handle them locally:

- forbidden
- not found
- duplicate nickname
- wrong password
- empty title
- deletion blocked by domain rules

Example:

```ts
type GetPostResult =
| { status: "success"; post: Post }
| { status: "not_found" }
| { status: "forbidden" };
```

React Query mutation/query functions may still throw for request failures, but
field-level or domain-level validation should usually render near the relevant
UI instead of going to a global ErrorBoundary.

## Suspense Placement

Suspense is a UI reveal-boundary tool, not a blanket replacement for all loading
states.

Use Suspense when:

- a page or large section is not ready to show on first entry
- the user expects a group of UI to appear together
- a centralized skeleton improves perceived stability

Avoid wrapping every API-calling component independently. That can make the page
appear in scattered timing and weaken information hierarchy.

Do not use Suspense for:

- button pending state
- form submit pending state
- tab/filter/search interactions
- background refetch where existing content should stay visible
- optimistic updates

For background refetch, preserve current data and show a small updating signal:

```tsx
<>
{isFetching ? <span>Updating...</span> : null}
<PostList posts={posts} />
</>
```

## Review Checklist

When these topics are in scope, check:

- Is the Server/Client boundary as low as practical?
- Did route/page files stay thin and metadata-capable?
- Is loading/error ownership local, sectional, or route-level by design?
- Are expected business failures modeled explicitly instead of hidden in
`catch` blocks?
- Is Suspense placed around a user-meaningful reveal unit?
- Are background refetch and optimistic updates handled without replacing
stable content with skeletons?
18 changes: 18 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
## 요약

<!-- 이 PR에서 무엇을 변경했는지 간단히 적어주세요. -->

## 변경 사항

-

## 확인 사항

- [ ] 변경한 기능을 로컬에서 직접 실행하고 예상 결과와 일치하는지 확인했습니다.
- [ ] 변경한 기능과 연결된 기존 기능이 깨지지 않았는지 확인했습니다.
- [ ] 로딩, 빈 데이터, 오류 또는 잘못된 입력 상황을 확인했습니다.
- [ ] GitHub Actions의 CI가 모두 통과했습니다.

## 스크린샷

<!-- UI 변경이 있는 경우 첨부해주세요. 없다면 이 항목을 삭제해도 됩니다. -->
Loading