@potenlab/ui 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +18 -1
  2. package/dist/cli.js +756 -0
  3. package/package.json +8 -3
  4. package/template/admin/README.md +36 -0
  5. package/template/admin/_gitignore +41 -0
  6. package/template/admin/components.json +23 -0
  7. package/template/admin/docs/changes.json +295 -0
  8. package/template/admin/docs/dev-plan.md +822 -0
  9. package/template/admin/docs/frontend-plan.md +874 -0
  10. package/template/admin/docs/prd.md +408 -0
  11. package/template/admin/docs/progress.json +777 -0
  12. package/template/admin/docs/test-plan.md +790 -0
  13. package/template/admin/docs/ui-ux-plan.md +1664 -0
  14. package/template/admin/eslint.config.mjs +18 -0
  15. package/template/admin/next.config.ts +7 -0
  16. package/template/admin/package.json +43 -0
  17. package/template/admin/postcss.config.mjs +7 -0
  18. package/template/admin/public/avatars/user1.svg +4 -0
  19. package/template/admin/public/avatars/user2.svg +4 -0
  20. package/template/admin/public/avatars/user3.svg +4 -0
  21. package/template/admin/public/avatars/user4.svg +4 -0
  22. package/template/admin/public/avatars/user5.svg +4 -0
  23. package/template/admin/public/file.svg +1 -0
  24. package/template/admin/public/globe.svg +1 -0
  25. package/template/admin/public/next.svg +1 -0
  26. package/template/admin/public/profile/img1.svg +7 -0
  27. package/template/admin/public/profile/img2.svg +7 -0
  28. package/template/admin/public/profile/img3.svg +7 -0
  29. package/template/admin/public/vercel.svg +1 -0
  30. package/template/admin/public/window.svg +1 -0
  31. package/template/admin/src/app/favicon.ico +0 -0
  32. package/template/admin/src/app/layout.tsx +38 -0
  33. package/template/admin/src/app/page.tsx +5 -0
  34. package/template/admin/src/app/users/[id]/page.tsx +10 -0
  35. package/template/admin/src/components/layouts/app-sidebar.tsx +152 -0
  36. package/template/admin/src/components/user-management/profile-images.tsx +69 -0
  37. package/template/admin/src/components/user-management/user-detail-form.tsx +143 -0
  38. package/template/admin/src/features/user-management/components/user-columns.tsx +101 -0
  39. package/template/admin/src/features/user-management/components/user-detail.tsx +79 -0
  40. package/template/admin/src/features/user-management/components/user-list.tsx +74 -0
  41. package/template/admin/src/features/user-management/types/index.ts +113 -0
  42. package/template/admin/src/features/user-management/utils/format.ts +2 -0
  43. package/template/admin/src/lib/mock-data.ts +131 -0
  44. package/template/admin/src/styles/globals.css +26 -0
  45. package/template/admin/tsconfig.json +34 -0
@@ -0,0 +1,101 @@
1
+ "use client";
2
+
3
+ import type { ColumnDef } from "@tanstack/react-table";
4
+ import { Avatar, AvatarImage, AvatarFallback, Button, DataTableColumnHeader } from "@potenlab/ui";
5
+ import type { User } from "@/features/user-management/types";
6
+
7
+ interface GetUserColumnsOptions {
8
+ onDelete?: (user: User) => void;
9
+ }
10
+
11
+ export function getUserColumns(
12
+ options?: GetUserColumnsOptions
13
+ ): ColumnDef<User>[] {
14
+ return [
15
+ {
16
+ accessorKey: "nickname",
17
+ header: "닉네임",
18
+ meta: { className: "flex-1" },
19
+ },
20
+ {
21
+ accessorKey: "grade",
22
+ header: "등급",
23
+ meta: { className: "flex-1" },
24
+ },
25
+ {
26
+ accessorKey: "avatar",
27
+ header: "아바타",
28
+ meta: { className: "w-[80px] text-center", headerClassName: "w-[80px] text-center" },
29
+ cell: ({ row }) => (
30
+ <div className="flex items-center justify-center">
31
+ <Avatar>
32
+ <AvatarImage
33
+ src={row.original.avatar}
34
+ alt={`${row.original.nickname} avatar`}
35
+ />
36
+ <AvatarFallback>{row.original.nickname.charAt(0)}</AvatarFallback>
37
+ </Avatar>
38
+ </div>
39
+ ),
40
+ },
41
+ {
42
+ accessorKey: "phone",
43
+ header: "휴대폰 번호",
44
+ meta: { className: "flex-1" },
45
+ },
46
+ {
47
+ accessorKey: "age",
48
+ header: "나이",
49
+ meta: { className: "flex-1" },
50
+ },
51
+ {
52
+ accessorKey: "gender",
53
+ header: "성별",
54
+ meta: { className: "flex-1" },
55
+ },
56
+ {
57
+ accessorKey: "region",
58
+ header: "지역",
59
+ meta: { className: "flex-1" },
60
+ },
61
+ {
62
+ accessorKey: "joinDate",
63
+ header: ({ column }) => (
64
+ <DataTableColumnHeader column={column} title="가입일" />
65
+ ),
66
+ meta: { className: "flex-1" },
67
+ enableSorting: true,
68
+ },
69
+ {
70
+ accessorKey: "withdrawalDate",
71
+ header: ({ column }) => (
72
+ <DataTableColumnHeader column={column} title="탈퇴일" />
73
+ ),
74
+ meta: { className: "flex-1" },
75
+ enableSorting: true,
76
+ },
77
+ {
78
+ id: "delete",
79
+ header: "삭제",
80
+ meta: { className: "w-[57px]" },
81
+ cell: ({ row }) => (
82
+ <Button
83
+ variant="ghost"
84
+ size="ghost"
85
+ onClick={(e) => {
86
+ e.stopPropagation();
87
+ options?.onDelete?.(row.original);
88
+ }}
89
+ onKeyDown={(e) => {
90
+ if (e.key === "Enter" || e.key === " ") {
91
+ e.stopPropagation();
92
+ }
93
+ }}
94
+ aria-label={`Delete user ${row.original.nickname}`}
95
+ >
96
+ 삭제
97
+ </Button>
98
+ ),
99
+ },
100
+ ];
101
+ }
@@ -0,0 +1,79 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { useForm } from "react-hook-form";
5
+ import { Pencil } from "lucide-react";
6
+
7
+ import { Card, CardContent, PageHeader } from "@potenlab/ui";
8
+ import { UserDetailForm } from "@/components/user-management/user-detail-form";
9
+ import { mockUsers } from "@/lib/mock-data";
10
+ import type { UserDetailFormValues } from "@/features/user-management/types";
11
+
12
+ export interface UserDetailProps {
13
+ userId: string;
14
+ }
15
+
16
+ export function UserDetail({ userId }: UserDetailProps) {
17
+ const user = mockUsers.find((u) => u.id === userId) || mockUsers[0];
18
+
19
+ const [profileImages, setProfileImages] = useState<string[]>(user.profileImages);
20
+
21
+ const form = useForm<UserDetailFormValues>({
22
+ defaultValues: {
23
+ role: user.role,
24
+ nickname: user.nickname,
25
+ phone: user.phone,
26
+ age: user.age,
27
+ gender: user.gender,
28
+ exerciseStyle: user.exerciseStyle,
29
+ gymRelocation: user.gymRelocation,
30
+ region: user.region,
31
+ bench: user.bench,
32
+ deadlift: user.deadlift,
33
+ squat: user.squat,
34
+ intro: user.intro,
35
+ profilePublic: user.settings.profilePublic,
36
+ matchChatNotification: user.settings.matchChatNotification,
37
+ marketingNotification: user.settings.marketingNotification,
38
+ },
39
+ });
40
+
41
+ const handleImageUpload = (index: number, file: File) => {
42
+ const previewUrl = URL.createObjectURL(file);
43
+ setProfileImages((prev) => {
44
+ const updated = [...prev];
45
+ if (index < updated.length) {
46
+ updated[index] = previewUrl;
47
+ } else {
48
+ updated.push(previewUrl);
49
+ }
50
+ return updated;
51
+ });
52
+ };
53
+
54
+ const handleSave = form.handleSubmit((data) => {
55
+ // TODO: implement save logic
56
+ console.log("Save:", data);
57
+ });
58
+
59
+ return (
60
+ <Card>
61
+ <CardContent className="p-8">
62
+ <PageHeader
63
+ title="사용자관리"
64
+ subtitle="사용자 정보를 수정할 수 있습니다."
65
+ actionLabel="변경사항 저장"
66
+ actionIcon={Pencil}
67
+ onAction={handleSave}
68
+ />
69
+
70
+ <UserDetailForm
71
+ user={user}
72
+ form={form}
73
+ profileImages={profileImages}
74
+ onImageUpload={handleImageUpload}
75
+ />
76
+ </CardContent>
77
+ </Card>
78
+ );
79
+ }
@@ -0,0 +1,74 @@
1
+ "use client";
2
+
3
+ import { useState, useMemo } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { Pencil } from "lucide-react";
6
+
7
+ import { Card, CardContent, PageHeader, TabNavigation, SearchBar, PaginationControls, DataTable } from "@potenlab/ui";
8
+ import { getUserColumns } from "@/features/user-management/components/user-columns";
9
+ import { mockUsers, totalUserCount } from "@/lib/mock-data";
10
+
11
+ export function UserList() {
12
+ const router = useRouter();
13
+ const [searchValue, setSearchValue] = useState("");
14
+ const [activeTab, setActiveTab] = useState("all");
15
+ const [currentPage, setCurrentPage] = useState(1);
16
+ const [itemsPerPage, setItemsPerPage] = useState(10);
17
+
18
+ const columns = useMemo(
19
+ () =>
20
+ getUserColumns({
21
+ onDelete: (user) => {
22
+ // TODO: implement delete confirmation dialog
23
+ console.log("Delete user:", user.id);
24
+ },
25
+ }),
26
+ []
27
+ );
28
+
29
+ return (
30
+ <Card>
31
+ <CardContent className="p-8">
32
+ <div className="flex flex-col gap-6">
33
+ <PageHeader
34
+ title="사용자관리"
35
+ subtitle="사용자 리스트 관리 페이지"
36
+ badgeCount={totalUserCount}
37
+ actionLabel="작성"
38
+ actionIcon={Pencil}
39
+ />
40
+
41
+ <TabNavigation
42
+ tabs={[
43
+ { value: "all", label: "전체" },
44
+ { value: "tab2", label: "Tab" },
45
+ { value: "tab3", label: "Tab" },
46
+ ]}
47
+ defaultValue={activeTab}
48
+ onTabChange={setActiveTab}
49
+ />
50
+
51
+ <SearchBar
52
+ value={searchValue}
53
+ onChange={setSearchValue}
54
+ />
55
+
56
+ <PaginationControls
57
+ currentPage={currentPage}
58
+ totalPages={1}
59
+ itemsPerPage={itemsPerPage}
60
+ onPageChange={setCurrentPage}
61
+ onItemsPerPageChange={setItemsPerPage}
62
+ />
63
+
64
+ <DataTable
65
+ columns={columns}
66
+ data={mockUsers}
67
+ enableSorting
68
+ onRowClick={(user) => router.push(`/users/${user.id}`)}
69
+ />
70
+ </div>
71
+ </CardContent>
72
+ </Card>
73
+ );
74
+ }
@@ -0,0 +1,113 @@
1
+ // src/features/user-management/types/index.ts
2
+
3
+ export interface UserSettings {
4
+ profilePublic: boolean;
5
+ matchChatNotification: boolean;
6
+ marketingNotification: boolean;
7
+ }
8
+
9
+ export interface User {
10
+ id: string;
11
+ nickname: string;
12
+ grade: string;
13
+ avatar: string;
14
+ phone: string;
15
+ age: string;
16
+ gender: string;
17
+ region: string;
18
+ joinDate: string;
19
+ withdrawalDate: string;
20
+ role: string;
21
+ exerciseStyle: string;
22
+ gymRelocation: string;
23
+ bench: string;
24
+ deadlift: string;
25
+ squat: string;
26
+ intro: string;
27
+ profileImages: string[];
28
+ settings: UserSettings;
29
+ }
30
+
31
+ export interface UserDetailFormValues {
32
+ role: string;
33
+ nickname: string;
34
+ phone: string;
35
+ age: string;
36
+ gender: string;
37
+ exerciseStyle: string;
38
+ gymRelocation: string;
39
+ region: string;
40
+ bench: string;
41
+ deadlift: string;
42
+ squat: string;
43
+ intro: string;
44
+ profilePublic: boolean;
45
+ matchChatNotification: boolean;
46
+ marketingNotification: boolean;
47
+ }
48
+
49
+ /** @deprecated Use ColumnDef<User> from @tanstack/react-table with getUserColumns() instead */
50
+ export interface UserTableColumn {
51
+ key: keyof User | "delete";
52
+ label: string;
53
+ width: string; // CSS width: "flex-1", "80px", "57px"
54
+ sortable: boolean;
55
+ alignment: "left" | "center";
56
+ }
57
+
58
+ /** @deprecated Use getUserColumns() from @/features/user-management/components/user-columns instead */
59
+ export const USER_TABLE_COLUMNS: UserTableColumn[] = [
60
+ { key: "nickname", label: "닉네임", width: "flex-1", sortable: false, alignment: "left" },
61
+ { key: "grade", label: "등급", width: "flex-1", sortable: false, alignment: "left" },
62
+ { key: "avatar", label: "아바타", width: "80px", sortable: false, alignment: "center" },
63
+ { key: "phone", label: "휴대폰 번호", width: "flex-1", sortable: false, alignment: "left" },
64
+ { key: "age", label: "나이", width: "flex-1", sortable: false, alignment: "left" },
65
+ { key: "gender", label: "성별", width: "flex-1", sortable: false, alignment: "left" },
66
+ { key: "region", label: "지역", width: "flex-1", sortable: false, alignment: "left" },
67
+ { key: "joinDate", label: "가입일", width: "flex-1", sortable: true, alignment: "left" },
68
+ { key: "withdrawalDate", label: "탈퇴일", width: "flex-1", sortable: true, alignment: "left" },
69
+ { key: "delete", label: "삭제", width: "57px", sortable: false, alignment: "left" },
70
+ ];
71
+
72
+ export interface PaginationState {
73
+ currentPage: number;
74
+ totalPages: number;
75
+ itemsPerPage: number;
76
+ pageJumpInput: string;
77
+ }
78
+
79
+ export const DEFAULT_PAGINATION: PaginationState = {
80
+ currentPage: 1,
81
+ totalPages: 1,
82
+ itemsPerPage: 10,
83
+ pageJumpInput: "",
84
+ };
85
+
86
+ export type FormFieldType = "input" | "select";
87
+
88
+ export interface FormField {
89
+ key: keyof User;
90
+ label: string;
91
+ type: FormFieldType;
92
+ options?: string[]; // Only for type "select"
93
+ }
94
+
95
+ export const BASIC_INFO_ROW_1: FormField[] = [
96
+ { key: "role", label: "역할", type: "select", options: ["사용자", "관리자"] },
97
+ { key: "nickname", label: "닉네임", type: "input" },
98
+ { key: "phone", label: "휴대폰", type: "input" },
99
+ { key: "age", label: "나이", type: "input" },
100
+ ];
101
+
102
+ export const BASIC_INFO_ROW_2: FormField[] = [
103
+ { key: "gender", label: "성별", type: "select", options: ["남자", "여자"] },
104
+ { key: "exerciseStyle", label: "운동 스타일", type: "select", options: ["보디빌딩", "크로스핏", "유산소"] },
105
+ { key: "gymRelocation", label: "헬스장 이전", type: "select", options: ["가능", "불가능"] },
106
+ { key: "region", label: "지역", type: "select", options: ["서울 마포구", "강남구", "송파구"] },
107
+ ];
108
+
109
+ export const BASIC_INFO_ROW_3: FormField[] = [
110
+ { key: "bench", label: "벤치프레스", type: "input" },
111
+ { key: "deadlift", label: "데드리프트", type: "input" },
112
+ { key: "squat", label: "스쿼트", type: "input" },
113
+ ];
@@ -0,0 +1,2 @@
1
+ /** @deprecated Import from "@potenlab/ui" instead */
2
+ export { formatNumber } from "@potenlab/ui";
@@ -0,0 +1,131 @@
1
+ import type { User } from "@/features/user-management/types";
2
+
3
+ export const mockUsers: User[] = [
4
+ {
5
+ id: "1",
6
+ nickname: "닉네임입니다.",
7
+ grade: "매니아",
8
+ avatar: "/avatars/user1.svg",
9
+ phone: "010-1234-1234",
10
+ age: "1999년생",
11
+ gender: "남자",
12
+ region: "강남구",
13
+ joinDate: "2022년 11월 1일",
14
+ withdrawalDate: "2022년 11월 1일",
15
+ role: "사용자",
16
+ exerciseStyle: "보디빌딩",
17
+ gymRelocation: "가능",
18
+ bench: "100kg",
19
+ deadlift: "100kg",
20
+ squat: "100kg",
21
+ intro: "한줄 소개 내용입니다.",
22
+ profileImages: ["/profile/img1.svg", "/profile/img2.svg", "/profile/img3.svg"],
23
+ settings: {
24
+ profilePublic: false,
25
+ matchChatNotification: true,
26
+ marketingNotification: false,
27
+ },
28
+ },
29
+ {
30
+ id: "2",
31
+ nickname: "닉네임입니다.",
32
+ grade: "매니아",
33
+ avatar: "/avatars/user2.svg",
34
+ phone: "010-1234-1234",
35
+ age: "1999년생",
36
+ gender: "남자",
37
+ region: "강남구",
38
+ joinDate: "2022년 11월 1일",
39
+ withdrawalDate: "2022년 11월 1일",
40
+ role: "사용자",
41
+ exerciseStyle: "보디빌딩",
42
+ gymRelocation: "가능",
43
+ bench: "100kg",
44
+ deadlift: "100kg",
45
+ squat: "100kg",
46
+ intro: "한줄 소개 내용입니다.",
47
+ profileImages: ["/profile/img1.svg", "/profile/img2.svg", "/profile/img3.svg"],
48
+ settings: {
49
+ profilePublic: false,
50
+ matchChatNotification: true,
51
+ marketingNotification: false,
52
+ },
53
+ },
54
+ {
55
+ id: "3",
56
+ nickname: "닉네임입니다.",
57
+ grade: "매니아",
58
+ avatar: "/avatars/user3.svg",
59
+ phone: "010-1234-1234",
60
+ age: "1999년생",
61
+ gender: "남자",
62
+ region: "강남구",
63
+ joinDate: "2022년 11월 1일",
64
+ withdrawalDate: "2022년 11월 1일",
65
+ role: "사용자",
66
+ exerciseStyle: "보디빌딩",
67
+ gymRelocation: "가능",
68
+ bench: "100kg",
69
+ deadlift: "100kg",
70
+ squat: "100kg",
71
+ intro: "한줄 소개 내용입니다.",
72
+ profileImages: ["/profile/img1.svg", "/profile/img2.svg", "/profile/img3.svg"],
73
+ settings: {
74
+ profilePublic: false,
75
+ matchChatNotification: true,
76
+ marketingNotification: false,
77
+ },
78
+ },
79
+ {
80
+ id: "4",
81
+ nickname: "닉네임입니다.",
82
+ grade: "매니아",
83
+ avatar: "/avatars/user4.svg",
84
+ phone: "010-1234-1234",
85
+ age: "1999년생",
86
+ gender: "남자",
87
+ region: "강남구",
88
+ joinDate: "2022년 11월 1일",
89
+ withdrawalDate: "2022년 11월 1일",
90
+ role: "사용자",
91
+ exerciseStyle: "보디빌딩",
92
+ gymRelocation: "가능",
93
+ bench: "100kg",
94
+ deadlift: "100kg",
95
+ squat: "100kg",
96
+ intro: "한줄 소개 내용입니다.",
97
+ profileImages: ["/profile/img1.svg", "/profile/img2.svg", "/profile/img3.svg"],
98
+ settings: {
99
+ profilePublic: false,
100
+ matchChatNotification: true,
101
+ marketingNotification: false,
102
+ },
103
+ },
104
+ {
105
+ id: "5",
106
+ nickname: "닉네임입니다.",
107
+ grade: "매니아",
108
+ avatar: "/avatars/user5.svg",
109
+ phone: "010-1234-1234",
110
+ age: "1999년생",
111
+ gender: "남자",
112
+ region: "강남구",
113
+ joinDate: "2022년 11월 1일",
114
+ withdrawalDate: "2022년 11월 1일",
115
+ role: "사용자",
116
+ exerciseStyle: "보디빌딩",
117
+ gymRelocation: "가능",
118
+ bench: "100kg",
119
+ deadlift: "100kg",
120
+ squat: "100kg",
121
+ intro: "한줄 소개 내용입니다.",
122
+ profileImages: ["/profile/img1.svg", "/profile/img2.svg", "/profile/img3.svg"],
123
+ settings: {
124
+ profilePublic: false,
125
+ matchChatNotification: true,
126
+ marketingNotification: false,
127
+ },
128
+ },
129
+ ];
130
+
131
+ export const totalUserCount = 100000;
@@ -0,0 +1,26 @@
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+ @import "shadcn/tailwind.css";
4
+ @import "@potenlab/ui/styles/globals.css";
5
+ @source "../node_modules/@potenlab/ui/dist";
6
+
7
+ @layer base {
8
+ * {
9
+ @apply border-border outline-ring/50;
10
+ }
11
+ body {
12
+ @apply bg-background text-foreground;
13
+ background-color: #FCFCFC;
14
+ font-family: 'Pretendard Variable', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
15
+ }
16
+ }
17
+
18
+ @media (prefers-reduced-motion: reduce) {
19
+ *,
20
+ *::before,
21
+ *::after {
22
+ animation-duration: 0.01ms !important;
23
+ animation-iteration-count: 1 !important;
24
+ transition-duration: 0.01ms !important;
25
+ }
26
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "react-jsx",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./src/*"]
23
+ }
24
+ },
25
+ "include": [
26
+ "next-env.d.ts",
27
+ "**/*.ts",
28
+ "**/*.tsx",
29
+ ".next/types/**/*.ts",
30
+ ".next/dev/types/**/*.ts",
31
+ "**/*.mts"
32
+ ],
33
+ "exclude": ["node_modules"]
34
+ }