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๋ฅผ ์ฒ๋ฆฌํ์ธ์.
๊ด๋ จ ๋ฌธ์