메인 콘텐츠로 건너뛰기
Sonamu는 Entity 정의를 기반으로 TypeScript 타입, Zod 스키마, API 클라이언트, React 컴포넌트 등을 자동으로 생성합니다. 이 문서는 생성되는 모든 파일의 종류와 용도를 설명합니다.

생성 파일 개요

타입 & 스키마

TypeScript 타입과 Zod 스키마*.types.ts, sonamu.generated.ts

API 클라이언트

HTTP 클라이언트 함수services.generated.ts

쿼리 헬퍼

Subset 쿼리 함수들sonamu.generated.sso.ts

React 컴포넌트

Form, List, Select 컴포넌트view_*.tsx

핵심 생성 파일

1. Entity Types ({entity}.types.ts)

Entity별 TypeScript 타입과 Zod 스키마가 생성됩니다.
api/src/application/user/user.types.ts (자동 생성)
import { z } from "zod";

// Base 타입
export type User = {
  id: number;
  email: string;
  username: string;
  role: "admin" | "normal";
  created_at: Date;
};

// Zod 스키마
export const User = z.object({
  id: z.number(),
  email: z.string(),
  username: z.string(),
  role: z.enum(["admin", "normal"]),
  created_at: z.date(),
});

// List 파라미터
export const UserListParams = z.object({
  num: z.number().optional(),
  page: z.number().optional(),
  search: UserSearchField.optional(),
  keyword: z.string().optional(),
  orderBy: UserOrderBy.optional(),
});

// Save 파라미터
export const UserSaveParams = User.partial({ id: true });
생성 시점: Entity 저장 또는 pnpm sonamu sync 수정 가능 여부: ❌ 자동 재생성됨 (커스텀 타입은 별도 파일에)

2. Generated Base (sonamu.generated.ts)

프로젝트 전체의 기본 타입과 Enum이 생성됩니다.
api/src/application/sonamu.generated.ts (자동 생성)
import { z } from "zod";

// 모든 Entity의 Enum
export const UserRole = z.enum(["admin", "normal"]);
export type UserRole = z.infer<typeof UserRole>;

export const UserSearchField = z.enum(["id", "email", "username"]);
export const UserOrderBy = z.enum(["id-desc", "id-asc", "created_at-desc"]);

// Subset 타입
export type UserSubsetKey = "A" | "P" | "SS";
export type UserSubsetMapping = {
  A: UserA;
  P: UserP;
  SS: UserSS;
};

// Enum 라벨 헬퍼
export function userRoleLabel(role: UserRole): string {
  return {
    admin: "관리자",
    normal: "일반 사용자",
  }[role];
}

// Export all models
export * from "./user/user.model";
export * from "./post/post.model";
생성 시점: Entity 저장 또는 pnpm sonamu sync 수정 가능 여부: ❌ 자동 재생성됨

3. Subset Queries (sonamu.generated.sso.ts)

Subset별 쿼리 함수가 생성됩니다.
api/src/application/sonamu.generated.sso.ts (자동 생성)
import type { PuriWrapper } from "sonamu";

// Subset 쿼리 함수들
export const userSubsetQueries = {
  A: (puri: PuriWrapper) =>
    puri
      .table("users")
      .select([
        "users.id",
        "users.email",
        "users.username",
        "users.role",
        "users.created_at",
      ]),
      
  P: (puri: PuriWrapper) =>
    puri
      .table("users")
      .select([
        "users.id",
        "users.email",
        "users.username",
      ])
      .leftJoin("employees", "employees.user_id", "users.id")
      .select([
        "employees.id as employee__id",
        "employees.department_id as employee__department_id",
      ]),
      
  SS: (puri: PuriWrapper) =>
    puri
      .table("users")
      .select(["users.id", "users.email"]),
};

// Loader 쿼리 함수들 (HasMany, ManyToMany)
export const userLoaderQueries = {
  P: [
    {
      as: "posts",
      refId: "id",
      qb: (puri: PuriWrapper, ids: number[]) =>
        puri
          .table("posts")
          .whereIn("posts.user_id", ids)
          .select([
            "posts.id",
            "posts.user_id as refId",
            "posts.title",
          ]),
    },
  ],
};
생성 시점: Entity의 Subset 변경 시 수정 가능 여부: ❌ 자동 재생성됨

4. API Services (services.generated.ts)

API 클라이언트 함수가 생성됩니다.
web/src/services/services.generated.ts (자동 생성)
import axios from "axios";
import type { ListResult } from "./sonamu.shared";
import type { User, UserListParams, UserSaveParams } from "./user/user.types";

// Axios 클라이언트
export async function findUserById(id: number): Promise<User> {
  const { data } = await axios.get("/user/findById", { params: { id } });
  return data;
}

export async function findManyUsers(
  params?: UserListParams
): Promise<ListResult<UserListParams, User>> {
  const { data } = await axios.get("/user/findMany", { params });
  return data;
}

export async function saveUser(params: UserSaveParams[]): Promise<number[]> {
  const { data } = await axios.post("/user/save", { params });
  return data;
}

// TanStack Query Hooks
export function useUserById(id: number) {
  return useQuery({
    queryKey: ["User", "findById", id],
    queryFn: () => findUserById(id),
  });
}

export function useUsers(params?: UserListParams) {
  return useQuery({
    queryKey: ["Users", "findMany", params],
    queryFn: () => findManyUsers(params),
  });
}

// TanStack Mutation Hooks
export function useSaveUser() {
  return useMutation({
    mutationFn: (params: UserSaveParams[]) => saveUser(params),
  });
}
생성 시점: Model의 @api 데코레이터 변경 시 수정 가능 여부: ❌ 자동 재생성됨
타겟별 생성: sonamu.config.tssync.targets에 지정된 각 타겟(web, app 등)에 복사됩니다.

5. HTTP Test File (sonamu.generated.http)

REST Client용 HTTP 테스트 파일이 생성됩니다.
api/sonamu.generated.http (자동 생성)
@baseUrl = http://localhost:3000

### User.findById
GET {{baseUrl}}/user/findById?id=1

### User.findMany
GET {{baseUrl}}/user/findMany?num=10&page=1

### User.save
POST {{baseUrl}}/user/save
Content-Type: application/json

{
  "params": [
    {
      "email": "[email protected]",
      "username": "Test User"
    }
  ]
}

### User.del
DELETE {{baseUrl}}/user/del
Content-Type: application/json

{
  "ids": [1, 2, 3]
}
생성 시점: Model 파일 변경 시 수정 가능 여부: ❌ 자동 재생성됨 사용법: VS Code의 REST Client 확장 설치 후 요청 실행

React 컴포넌트

Scaffold로 React UI 컴포넌트를 생성할 수 있습니다.

6. List Component

import { useUsers } from "@/services/services.generated";

export function UserList() {
  const [params, setParams] = useState({ num: 20, page: 1 });
  const { data, isLoading } = useUsers(params);

  return (
    <div>
      <table>
        <thead>
          <tr>
            <th>ID</th>
            <th>Email</th>
            <th>Username</th>
          </tr>
        </thead>
        <tbody>
          {data?.rows.map((user) => (
            <tr key={user.id}>
              <td>{user.id}</td>
              <td>{user.email}</td>
              <td>{user.username}</td>
            </tr>
          ))}
        </tbody>
      </table>
      
      <Pagination
        total={data?.total ?? 0}
        page={params.page}
        onChange={(page) => setParams({ ...params, page })}
      />
    </div>
  );
}
생성 시점: pnpm sonamu generate view_list --entity User 수정 가능 여부: ✅ 한번 생성 후 수정 가능

7. Form Component

web/src/pages/user/UserForm.tsx
import { useSaveUser } from "@/services/services.generated";
import { UserSaveParams } from "@/services/user/user.types";

export function UserForm({ userId }: { userId?: number }) {
  const { data: user } = useUserById(userId);
  const { mutate: save } = useSaveUser();
  
  const handleSubmit = (values: UserSaveParams) => {
    save([values], {
      onSuccess: () => {
        alert("저장되었습니다");
      },
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="email"
        type="email"
        defaultValue={user?.email}
        required
      />
      <input
        name="username"
        defaultValue={user?.username}
        required
      />
      <select name="role" defaultValue={user?.role ?? "normal"}>
        <option value="admin">관리자</option>
        <option value="normal">일반 사용자</option>
      </select>
      <button type="submit">저장</button>
    </form>
  );
}
생성 시점: pnpm sonamu generate view_form --entity User 수정 가능 여부: ✅ 한번 생성 후 수정 가능

8. Select Components

// Async Select (검색 가능)
export function UserIdAsyncSelect({
  value,
  onChange,
}: {
  value?: number;
  onChange: (value: number) => void;
}) {
  const [keyword, setKeyword] = useState("");
  const { data } = useUsers({ keyword, num: 10 });

  return (
    <AsyncSelect
      value={value}
      onChange={onChange}
      onSearch={setKeyword}
      options={data?.rows.map((u) => ({
        value: u.id,
        label: u.email,
      }))}
    />
  );
}

SSR 관련 파일

Server-Side Rendering을 위한 파일들이 자동 생성됩니다.

9. Queries (queries.generated.ts)

web/src/queries.generated.ts (자동 생성)
import { queryOptions } from "@tanstack/react-query";
import { findUserById, findManyUsers } from "./services/services.generated";

export const userQueries = {
  findById: (id: number) =>
    queryOptions({
      queryKey: ["User", "findById", id],
      queryFn: () => findUserById(id),
    }),
    
  findMany: (params?: UserListParams) =>
    queryOptions({
      queryKey: ["Users", "findMany", params],
      queryFn: () => findManyUsers(params),
    }),
};
생성 시점: Model 파일 변경 시 수정 가능 여부: ❌ 자동 재생성됨

10. Entry Server (entry-server.generated.tsx)

web/src/entry-server.generated.tsx (자동 생성)
import { dehydrate, QueryClient } from "@tanstack/react-query";
import { userQueries } from "./queries.generated";

export async function loader({ params }) {
  const queryClient = new QueryClient();

  // SSR 데이터 프리페칭
  if (params.userId) {
    await queryClient.prefetchQuery(
      userQueries.findById(Number(params.userId))
    );
  }

  return {
    dehydratedState: dehydrate(queryClient),
  };
}
생성 시점: Model 파일 변경 시 수정 가능 여부: ❌ 자동 재생성됨

다국어 지원 파일

i18n 설정이 있을 때 생성됩니다.

11. Sonamu Dictionary (sd.generated.ts)

api/src/sd.generated.ts (자동 생성)
export const SD = {
  User: "사용자",
  user: {
    id: "ID",
    email: "이메일",
    username: "사용자명",
    role: "역할",
    created_at: "생성일시",
  },
  UserRole: {
    admin: "관리자",
    normal: "일반 사용자",
  },
} as const;
생성 시점: Entity 또는 i18n 파일 변경 시 수정 가능 여부: ❌ 자동 재생성됨 타겟: api, web, app 각각 생성

생성 파일 요약표

파일위치생성 시점수정 가능
{entity}.types.tsapi/src/application/{entity}/Entity 저장
sonamu.generated.tsapi/src/application/Entity 저장
sonamu.generated.sso.tsapi/src/application/Entity 저장
services.generated.tsweb/src/services/Model 변경
sonamu.generated.httpapi/Model 변경
queries.generated.tsweb/src/Model 변경
entry-server.generated.tsxweb/src/Model 변경
sd.generated.tsapi/src/, web/src/, app/src/Entity/i18n 변경
{Entity}List.tsxweb/src/pages/{entity}/Scaffold
{Entity}Form.tsxweb/src/pages/{entity}/Scaffold
{Entity}SearchInput.tsxweb/src/pages/{entity}/Scaffold
{Entity}IdAsyncSelect.tsxweb/src/components/{entity}/Scaffold
{EnumId}Select.tsxweb/src/components/{entity}/Scaffold
자동 재생성 파일: *.generated.* 파일은 절대 수정하지 마세요. 변경 사항이 자동으로 덮어씌워집니다.

다음 단계