OrderBySelectλ λͺ©λ‘ νμ΄μ§μμ μ λ ¬ κΈ°μ€μ μ ννλ μλ μμ± μ»΄ν¬λνΈμ
λλ€. λ°±μλμ OrderBy enumμ κΈ°λ°μΌλ‘ μμ±λλ©°, νκΈ λ μ΄λΈμ΄ μλμΌλ‘ λ§€νλ©λλ€.
ν΅μ¬ κΈ°λ₯
μλ μμ±
Entity μ μμμ μλ μμ±OrderBy enum κΈ°λ°
νκΈ λ μ΄λΈ
μλ λ μ΄λΈ λ§€νβIDμ΅μ μβ, βμμ±μΌμ€λ¦μ°¨μβ λ±
useListParams ν΅ν©
register ν¨ν΄ μ§μURL λκΈ°ν μλ
컀μ€ν°λ§μ΄μ§
μ€μΊν΄λ© νμΌμμ λ‘μ΄ μμ κ°λ₯
μλ μμ± μ‘°κ±΄
OrderBySelectλ λ°±μλμ OrderBy enumμ΄ μ μλλ©΄ μλμΌλ‘ μμ±λ©λλ€.
λ°±μλ μ μ
// user.types.ts (λ°±μλ)
import { z } from "zod";
export const UserOrderBy = z.enum([
"id-desc",
"id-asc",
"created_at-desc",
"created_at-asc",
"username-asc",
"username-desc",
]);
export const UserListParams = UserBaseListParams.extend({
orderBy: UserOrderBy.optional(),
});
μλ μμ±λλ νμΌ
μμΉ: web/src/components/user/UserOrderBySelect.tsx
μλ μμ±λλ λ μ΄λΈ: web/src/services/sonamu.generated.ts
export const UserOrderByLabel = {
"id-desc": "IDμ΅μ μ",
"id-asc": "IDμ€λλμ",
"created_at-desc": "μμ±μΌμ΅μ μ",
"created_at-asc": "μμ±μΌμ€λλμ",
"username-asc": "μ΄λ¦μ€λ¦μ°¨μ",
"username-desc": "μ΄λ¦λ΄λ¦Όμ°¨μ",
};
λ μ΄λΈ μμ± κ·μΉ
id-desc β βIDμ΅μ μβ
created_at-asc β βμμ±μΌμ€λλμβ
username-desc β βμ΄λ¦λ΄λ¦Όμ°¨μβ
νλλͺ
κ³Ό μ λ ¬ λ°©ν₯(asc/desc)μ μ‘°ν©νμ¬ μλ μμ±λ©λλ€.
κΈ°λ³Έ μ¬μ©λ²
useListParamsμ ν¨κ» μ¬μ©
import { useListParams } from "@sonamu-kit/react-components";
import { UserOrderBySelect } from "@/components/user/UserOrderBySelect";
import { UserOrderBy } from "@/services/sonamu.generated";
export function UserListPage() {
const { register } = useListParams(UserListParams, {
num: 24,
page: 1,
orderBy: UserOrderBy.options[0], // κΈ°λ³Έκ°: "id-desc"
});
return (
<div className="flex gap-2">
<UserOrderBySelect {...register("orderBy")} />
</div>
);
}
λμ:
- μ¬μ©μκ° μ λ ¬ μ΅μ
μ ν
register("orderBy")κ° μλμΌλ‘ URL μ
λ°μ΄νΈ
- API νΈμΆ μ
listParams.orderByκ° μ λ¬λ¨
- λ°±μλμμ μ λ ¬ μ μ©
export type UserOrderBySelectProps = {
value?: string;
onValueChange?: (value: string | null | undefined) => void;
placeholder?: string;
textPrefix?: string;
clearable?: boolean;
disabled?: boolean;
className?: string;
};
κΈ°λ³Έ μ¬μ© (register μμ΄)
import { useState } from "react";
import { UserOrderBySelect } from "@/components/user/UserOrderBySelect";
export function UserListPage() {
const [orderBy, setOrderBy] = useState<string>("id-desc");
return (
<UserOrderBySelect
value={orderBy}
onValueChange={(value) => setOrderBy(value ?? "id-desc")}
placeholder="μ λ ¬ κΈ°μ€"
/>
);
}
Props μμΈ
placeholder
μ νλμ§ μμμ λ νμλλ ν
μ€νΈμ
λλ€.
<UserOrderBySelect
{...register("orderBy")}
placeholder="μ λ ¬ μ ν" // κΈ°λ³Έκ°: "μ λ ¬"
/>
textPrefix
κ° μ΅μ
μμ λΆλ μ λμ¬μ
λλ€.
<UserOrderBySelect
{...register("orderBy")}
textPrefix="μ λ ¬: "
/>
// λ λλ§ κ²°κ³Ό:
// - μ λ ¬: IDμ΅μ μ
// - μ λ ¬: IDμ€λλμ
// - μ λ ¬: μμ±μΌμ΅μ μ
textPrefix νμ© μμλͺ©λ‘ νμ΄μ§ μλ¨μ νμ¬ μ λ ¬μ λͺ
νν νμνκ³ μΆμ λ μ μ©ν©λλ€:[μ λ ¬: IDμ΅μ μ βΌ]
clearable
βμ 체β μ΅μ
μ μΆκ°νμ¬ μ λ ¬μ μ΄κΈ°νν μ μμ΅λλ€.
<UserOrderBySelect
{...register("orderBy")}
clearable
/>
// λ λλ§ κ²°κ³Ό:
// - μ 체 β clearableλ‘ μΆκ°λ¨
// - IDμ΅μ μ
// - IDμ€λλμ
// - ...
μ¬μ© μλ리μ€:
- κΈ°λ³Έ μ λ ¬λ‘ λμκ°κ³ μΆμ λ
- μ λ ¬ μμ΄ λ°μ΄ν°λ² μ΄μ€ μμλλ‘ λ³΄κ³ μΆμ λ
disabled
μ»΄ν¬λνΈλ₯Ό λΉνμ±νν©λλ€.
<UserOrderBySelect
{...register("orderBy")}
disabled={isLoading} // λ‘λ© μ€μλ λ³κ²½ λΆκ°
/>
className
Tailwind CSS ν΄λμ€λ₯Ό μΆκ°ν©λλ€.
<UserOrderBySelect
{...register("orderBy")}
className="w-[200px] h-8 bg-white border-gray-300 text-xs"
/>
μ€μ μμ
μμ ν λͺ©λ‘ νν°
import { useListParams } from "@sonamu-kit/react-components";
import { Input } from "@sonamu-kit/react-components/components";
import { UserService } from "@/services/services.generated";
import { UserOrderBySelect } from "@/components/user/UserOrderBySelect";
import { UserSearchFieldSelect } from "@/components/user/UserSearchFieldSelect";
import { UserRoleSelect } from "@/components/user/UserRoleSelect";
export function UserListPage() {
const { listParams, register } = useListParams(UserListParams, {
num: 24,
page: 1,
search: "id" as const,
keyword: "",
orderBy: "id-desc" as const,
role: undefined,
});
const { data } = UserService.useUsers("A", listParams);
return (
<div>
{/* νν° μΉμ
*/}
<div className="flex gap-2 mb-4">
{/* κ²μ */}
<UserSearchFieldSelect
{...register("search")}
className="w-32"
/>
<Input
{...register("keyword")}
placeholder="κ²μμ΄"
className="w-64"
/>
{/* μν νν° */}
<UserRoleSelect
{...register("role")}
clearable
/>
{/* μ λ ¬ */}
<UserOrderBySelect
{...register("orderBy")}
textPrefix="μ λ ¬: "
className="w-[200px]"
/>
</div>
{/* ν
μ΄λΈ */}
<table>
{/* ... */}
</table>
</div>
);
}
λͺ¨λ°μΌ λ°μν
<div className="flex flex-col sm:flex-row gap-2">
<UserOrderBySelect
{...register("orderBy")}
className="w-full sm:w-[200px]" // λͺ¨λ°μΌ: μ 체 λλΉ, λ°μ€ν¬ν: 200px
/>
</div>
μ λ ¬ μ΅μ
μ νμ
import { UserOrderBy } from "@/services/sonamu.generated";
<div className="flex items-center gap-2">
<span className="text-sm text-gray-500">
{UserOrderBy.options.length}κ° μ λ ¬ μ΅μ
</span>
<UserOrderBySelect {...register("orderBy")} />
</div>
컀μ€ν°λ§μ΄μ§
OrderBySelectλ μ€μΊν΄λ© νμΌμ΄λ―λ‘ μμ λ‘κ² μμ ν μ μμ΅λλ€.
λ μ΄λΈ λ³κ²½
// UserOrderBySelect.tsx μμ
import { UserOrderByLabel } from "@/services/sonamu.generated";
// 컀μ€ν
λ μ΄λΈ κ°μ²΄ μμ±
const customLabels = {
...UserOrderByLabel,
"id-desc": "μ΅μ λ±λ‘μ", // κΈ°λ³Έ: "IDμ΅μ μ"
"created_at-asc": "μ€λλ μ", // κΈ°λ³Έ: "μμ±μΌμ€λλμ"
};
export function UserOrderBySelect({ ... }) {
return (
<Select ...>
<SelectContent>
{validOptions.map((key) => (
<SelectItem key={key} value={key}>
{(textPrefix ?? "") + customLabels[key]}
</SelectItem>
))}
</SelectContent>
</Select>
);
}
μμ΄μ½ μΆκ°
import ArrowUpIcon from "~icons/lucide/arrow-up";
import ArrowDownIcon from "~icons/lucide/arrow-down";
const getIcon = (key: string) => {
if (key.endsWith("-asc")) return <ArrowUpIcon className="w-3 h-3" />;
if (key.endsWith("-desc")) return <ArrowDownIcon className="w-3 h-3" />;
return null;
};
<SelectItem key={key} value={key}>
<div className="flex items-center gap-2">
{getIcon(key)}
{customLabels[key]}
</div>
</SelectItem>
νΉμ μ΅μ
μ¨κΈ°κΈ°
// "username" μ λ ¬ μ΅μ
λ§ νμ
const visibleOptions = validOptions.filter(key =>
key.includes("username")
);
<SelectContent>
{visibleOptions.map((key) => (
<SelectItem key={key} value={key}>
{UserOrderByLabel[key]}
</SelectItem>
))}
</SelectContent>
κ·Έλ£Ήν
const groupedOptions = {
"κΈ°λ³Έ": ["id-desc", "id-asc"],
"λ μ§": ["created_at-desc", "created_at-asc"],
"μ΄λ¦": ["username-asc", "username-desc"],
};
<SelectContent>
{Object.entries(groupedOptions).map(([group, options]) => (
<Fragment key={group}>
<SelectLabel>{group}</SelectLabel>
{options.map((key) => (
<SelectItem key={key} value={key}>
{UserOrderByLabel[key]}
</SelectItem>
))}
</Fragment>
))}
</SelectContent>
λ°±μλ μ²λ¦¬
OrderBySelectμμ μ νν κ°μ λ°±μλλ‘ μ λ¬λμ΄ μ€μ μ λ ¬μ΄ μ μ©λ©λλ€.
Modelμμ μ²λ¦¬
// user.model.ts (λ°±μλ)
async findMany<T extends UserSubsetKey, LP extends UserListParams>(
subset: T,
rawParams?: LP,
): Promise<ListResult<LP, UserSubsetMapping[T]>> {
const params = {
num: 24,
page: 1,
orderBy: "id-desc" as const,
...rawParams,
};
const { qb } = this.getSubsetQueries(subset);
// orderBy μ²λ¦¬
if (params.orderBy === "id-desc") {
qb.orderBy("users.id", "desc");
} else if (params.orderBy === "id-asc") {
qb.orderBy("users.id", "asc");
} else if (params.orderBy === "created_at-desc") {
qb.orderBy("users.created_at", "desc");
} else if (params.orderBy === "created_at-asc") {
qb.orderBy("users.created_at", "asc");
} else if (params.orderBy === "username-asc") {
qb.orderBy("users.username", "asc");
} else if (params.orderBy === "username-desc") {
qb.orderBy("users.username", "desc");
} else {
exhaustive(params.orderBy); // νμ
μμ μ± λ³΄μ₯
}
return this.executeSubsetQuery({ subset, qb, params });
}
exhaustive ν¨ν΄
exhaustive()λ λͺ¨λ κ²½μ°λ₯Ό μ²λ¦¬νλμ§ μ»΄νμΌ νμμ κ²μ¦ν©λλ€.
import { exhaustive } from "sonamu";
// OrderBy enumμ μ μ΅μ
"email-asc"λ₯Ό μΆκ°νλ©΄
// μ»΄νμΌ μλ¬ λ°μ β κ°λ°μκ° λλ½μ μ¦μ μ μ μμ
else {
exhaustive(params.orderBy); // β νμ
μλ¬: "email-asc" λ―Έμ²λ¦¬
}
μ¬μμ±
OrderBySelectλ μ΄κΈ° μμ± ν μλ μ¬μμ±λμ§ μμ΅λλ€.
μ¬μμ±μ΄ νμν κ²½μ°
- μ»΄ν¬λνΈ νμΌμ μμ ν κ²½μ°
- Entityμ OrderBy enumμ΄ ν¬κ² λ³κ²½λ κ²½μ°
μ¬μμ± λ°©λ²
# μ»΄ν¬λνΈ μμ
rm web/src/components/user/UserOrderBySelect.tsx
# κ°λ° μλ² μ¬μμ (μλ μ¬μμ±)
pnpm dev
μ¬μμ± μ μ£Όμ컀μ€ν°λ§μ΄μ§ν λ΄μ©μ μ¬μμ± μ μ¬λΌμ§λλ€. μ€μν μμ μ¬νμ λ³λλ‘ λ°±μ
νκ±°λ λ¬ΈμννμΈμ.
λ¬Έμ ν΄κ²°
OrderBySelectκ° μμ±λμ§ μμ
μμΈ: λ°±μλμ OrderBy enumμ΄ μ μλμ§ μμ
ν΄κ²°:
// user.types.ts (λ°±μλ)μ μΆκ°
export const UserOrderBy = z.enum([
"id-desc",
"id-asc",
]);
export const UserListParams = UserBaseListParams.extend({
orderBy: UserOrderBy.optional(),
});
λ μ΄λΈμ΄ νκΈλ‘ νμλμ§ μμ
μμΈ: sonamu.generated.tsκ° μ¬μμ±λμ§ μμ
ν΄κ²°:
# sonamu.lock μμ ν μ¬λκΈ°ν
rm api/sonamu.lock
pnpm sonamu sync
μ νν΄λ μ λ ¬μ΄ μ μ©λμ§ μμ
μμΈ: λ°±μλ Modelμμ orderBy μ²λ¦¬ λλ½
ν΄κ²°: user.model.tsμ findManyμμ params.orderByλ₯Ό μ²λ¦¬νμΈμ.
κ΄λ ¨ λ¬Έμ