Refine을 사용한 간단한 웹 애플리케이션 예제

웹 애플리케이션을 빠르게 개발하고 싶습니까? 당신은 바로 이곳에 있습니다! 나는 프런트엔드에 리파인을, 백엔드에 Supabase를 사용하여 간단한 영화 웹 애플리케이션을 개발할 것입니다. 계속 읽으셔야 합니다. 매우 간단한 방법으로 단계별로 설명하려고 노력할 것입니다.

1. 세부 설정



구체화 응용 프로그램을 설정하는 두 가지 대체 방법이 있습니다.

권장되는 방법은 superplate 도구를 사용하는 것입니다. superplate의 CLI 마법사를 사용하면 몇 초 만에 애플리케이션을 만들고 사용자 정의할 수 있습니다.

또는 create-react-app 도구를 사용하여 빈 React 애플리케이션을 만든 다음 npm을 통해 구체화 모듈을 추가할 수 있습니다.

superplate-cli를 사용하고 Supabase를 선택하겠습니다. 원하는 대로 다른 옵션을 사용자 정의할 수 있습니다.



2. 구체화로 관리자 패널 생성


  • supabaseClient.tsx에 Supabase url 및 키를 추가해야 합니다.
  • App.tsx에 사용자 정의 로그인 페이지 추가

  • App.tsx

    import { Refine } from "@pankod/refine";
    
    import "@pankod/refine/dist/styles.min.css";
    import { dataProvider } from "@pankod/refine-supabase";
    
    import authProvider from "./authProvider";
    import { supabaseClient } from "utility";
    import { Login } from "./pages/login";
    
    function App() {
      return (
        <Refine
          dataProvider={dataProvider(supabaseClient)}
          authProvider={authProvider}
          LoginPage={Login}
        ></Refine>
      );
    }
    
    export default App;
    
    


    로그인 페이지

    
    import React from "react";
    import {
      Row,
      Col,
      AntdLayout,
      Card,
      Typography,
      Form,
      Input,
      Button,
      Checkbox,
    } from "@pankod/refine";
    import "./styles.css";
    
    import { useLogin } from "@pankod/refine";
    
    const { Text, Title } = Typography;
    
    export interface ILoginForm {
      username: string;
      password: string;
      remember: boolean;
    }
    
    export const Login: React.FC = () => {
      const [form] = Form.useForm<ILoginForm>();
    
      const { mutate: login } = useLogin<ILoginForm>();
    
      const CardTitle = (
        <Title level={3} className="title">
          Sign in your account
        </Title>
      );
    
      return (
        <AntdLayout className="layout">
          <Row
            justify="center"
            align="middle"
            style={{
              height: "100vh",
            }}
          >
            <Col xs={22}>
              <div className="container">
                <div className="imageContainer">
                  <img src="./refine.svg" alt="Refine Logo" />
                </div>
                <Card title={CardTitle} headStyle={{ borderBottom: 0 }}>
                  <Form<ILoginForm>
                    layout="vertical"
                    form={form}
                    onFinish={(values) => {
                      login(values);
                    }}
                    requiredMark={false}
                    initialValues={{
                      remember: false,
                      email: "[email protected]",
                      password: "refineflix",
                    }}
                  >
                    <Form.Item
                      name="email"
                      label="Email"
                      rules={[{ required: true, type: "email" }]}
                    >
                      <Input size="large" placeholder="Email" />
                    </Form.Item>
                    <Form.Item
                      name="password"
                      label="Password"
                      rules={[{ required: true }]}
                      style={{ marginBottom: "12px" }}
                    >
                      <Input type="password" placeholder="●●●●●●●●" size="large" />
                    </Form.Item>
                    <div style={{ marginBottom: "12px" }}>
                      <Form.Item name="remember" valuePropName="checked" noStyle>
                        <Checkbox
                          style={{
                            fontSize: "12px",
                          }}
                        >
                          Remember me
                        </Checkbox>
                      </Form.Item>
    
                      <a
                        style={{
                          float: "right",
                          fontSize: "12px",
                        }}
                        href="#"
                      >
                        Forgot password?
                      </a>
                    </div>
                    <Button type="primary" size="large" htmlType="submit" block>
                      Sign in
                    </Button>
                  </Form>
                  <div style={{ marginTop: 8 }}>
                    <Text style={{ fontSize: 12 }}>
                      Don’t have an account?{" "}
                      <a href="#" style={{ fontWeight: "bold" }}>
                        Sign up
                      </a>
                    </Text>
                  </div>
                </Card>
              </div>
            </Col>
          </Row>
        </AntdLayout>
      );
    };
    
    



    .layout {
        background: radial-gradient(50% 50% at 50% 50%, #63386a 0%, #310438 100%);
        background-size: "cover";
      }
    
      .container {
        max-width: 408px;
        margin: auto;
      }
    
      .title {
        text-align: center;
        color: #626262;
        font-size: 30px;
        letter-spacing: -0.04em;
      }
    
      .imageContainer {
        display: flex;
        align-items: center;
        justify-content: center;
        margin-bottom: 16px;
      }
    
    


    로그인을 위해 기본 사용자를 사용할 수 있습니다.


  • App.tsx에 리소스를 추가하여 영화 목록 페이지 만들기

  • import { Refine, Resource } from "@pankod/refine";
    
    import "@pankod/refine/dist/styles.min.css";
    import { dataProvider } from "@pankod/refine-supabase";
    
    import authProvider from "./authProvider";
    import { supabaseClient } from "utility";
    
    import {
      AdminMovieList,
    } from "./pages/admin/movies";
    import { Login } from "./pages/login";
    
    function App() {
      return (
        <Refine
          dataProvider={dataProvider(supabaseClient)}
          authProvider={authProvider}
          LoginPage={Login}
    
        >
          <Resource
            name="movies"
            list={AdminMovieList}
            options={{
              route: "admin/movies",
            }}
          />
        </Refine>
      );
    }
    
    export default App;
    


  • AdminMovieList 페이지

  • import {
      List,
      Table,
      useTable,
      IResourceComponentsProps,
      Space,
      EditButton,
      ShowButton,
      getDefaultSortOrder,
      CreateButton,
      DeleteButton,
    } from "@pankod/refine";
    
    import { IMovies } from "interfaces";
    
    export const AdminMovieList: React.FC<IResourceComponentsProps> = () => {
      const { tableProps, sorter } = useTable<IMovies>({
        initialSorter: [
          {
            field: "id",
            order: "asc",
          },
        ],
      });
    
      return (
        <List pageHeaderProps={{ extra: <CreateButton /> }}>
          <Table {...tableProps} rowKey="id">
            <Table.Column
              key="id"
              dataIndex="id"
              title="ID"
              sorter
              defaultSortOrder={getDefaultSortOrder("id", sorter)}
            />
            <Table.Column key="name" dataIndex="name" title="name" sorter />
    
            <Table.Column<IMovies>
              title="Actions"
              dataIndex="actions"
              render={(_, record) => (
                <Space>
                  <EditButton hideText size="small" recordItemId={record.id} />
                  <ShowButton hideText size="small" recordItemId={record.id} />
                  <DeleteButton hideText size="small" recordItemId={record.id} />
                </Space>
              )}
            />
          </Table>
        </List>
      );
    };
    
    


  • 동영상 인터페이스

  • export interface IMovies {
      id: string;
      name: string;
      description: string;
      preload: string;
      director: string;
      stars: string;
      premiere: string;
      trailer: string;
      images: IFile[];
    }
    



  • 이제 만들기 페이지를 추가하겠습니다.

  •       <Resource
            name="movies"
            list={AdminMovieList}
            create={AdminMovieCreate}
            options={{
              route: "admin/movies",
            }}
          />
    



    import {
      Create,
      Form,
      Input,
      IResourceComponentsProps,
      Upload,
      useForm,
      RcFile,
    } from "@pankod/refine";
    import { IMovies } from "interfaces";
    import { supabaseClient, normalizeFile } from "utility";
    
    export const AdminMovieCreate: React.FC<IResourceComponentsProps> = () => {
      const { formProps, saveButtonProps } = useForm<IMovies>();
    
      return (
        <Create saveButtonProps={saveButtonProps}>
          <Form {...formProps} layout="vertical">
            <Form.Item
              label="Name"
              name="name"
              rules={[
                {
                  required: true,
                },
              ]}
            >
              <Input />
            </Form.Item>
            <Form.Item label="Premiere" name="premiere">
              <Input />
            </Form.Item>
            <Form.Item label="Description" name="description">
              <Input />
            </Form.Item>
            <Form.Item label="Director" name="director">
              <Input />
            </Form.Item>
            <Form.Item label="Stars" name="stars">
              <Input />
            </Form.Item>
    
            <Form.Item label="Images">
              <Form.Item
                name="images"
                valuePropName="fileList"
                normalize={normalizeFile}
                noStyle
              >
                <Upload.Dragger
                  name="file"
                  listType="picture"
                  multiple
                  customRequest={async ({ file, onError, onSuccess }) => {
                    try {
                      const rcFile = file as RcFile;
    
                      await supabaseClient.storage
                        .from("refineflix")
                        .upload(`public/${rcFile.name}`, file, {
                          cacheControl: "3600",
                          upsert: true,
                        });
    
                      const { data } = supabaseClient.storage
                        .from("refineflix")
                        .getPublicUrl(`public/${rcFile.name}`);
    
                      const xhr = new XMLHttpRequest();
                      onSuccess && onSuccess({ url: data?.publicURL }, xhr);
                    } catch (error) {
                      onError && onError(new Error("Upload Error"));
                    }
                  }}
                >
                  <p className="ant-upload-text">Drag & drop a file in this area</p>
                </Upload.Dragger>
              </Form.Item>
            </Form.Item>
          </Form>
        </Create>
      );
    };
    
    


  • 유틸리티 폴더의 파일 정규화

  • import { UploadFile } from "@pankod/refine";
    
    interface UploadResponse {
        url: string;
    }
    interface EventArgs<T = UploadResponse> {
        file: UploadFile<T>;
        fileList: Array<UploadFile<T>>;
    }
    
    export const normalizeFile = (event: EventArgs) => {
        const { fileList } = event;
    
        return fileList.map((item) => {
            const { uid, name, type, size, response, percent, status } = item;
    
            return {
                uid,
                name,
                url: item.url || response?.url,
                type,
                size,
                percent,
                status,
            };
        });
    };
    
    



  • 페이지 편집

  • import React from "react";
    import {
      Edit,
      Form,
      Input,
      IResourceComponentsProps,
      RcFile,
      Upload,
      useForm,
    } from "@pankod/refine";
    
    import { IMovies } from "interfaces";
    import { supabaseClient, normalizeFile } from "utility";
    
    export const AdminMovieEdit: React.FC<IResourceComponentsProps> = () => {
      const { formProps, saveButtonProps } = useForm<IMovies>();
    
      return (
        <Edit saveButtonProps={saveButtonProps} pageHeaderProps={{ extra: null }}>
          <Form {...formProps} layout="vertical">
            <Form.Item
              label="Name"
              name="name"
              rules={[
                {
                  required: true,
                },
              ]}
            >
              <Input />
            </Form.Item>
            <Form.Item label="Premiere" name="premiere">
              <Input />
            </Form.Item>
            <Form.Item label="Description" name="description">
              <Input />
            </Form.Item>
            <Form.Item label="Director" name="director">
              <Input />
            </Form.Item>
            <Form.Item label="Stars" name="stars">
              <Input />
            </Form.Item>
            <Form.Item label="Trailer" name="trailer">
              <Input />
            </Form.Item>
            <Form.Item label="Images">
              <Form.Item
                name="images"
                valuePropName="fileList"
                normalize={normalizeFile}
                noStyle
              >
                <Upload.Dragger
                  name="file"
                  listType="picture"
                  multiple
                  customRequest={async ({ file, onError, onSuccess }) => {
                    try {
                      const rcFile = file as RcFile;
    
                      await supabaseClient.storage
                        .from("refineflix")
                        .upload(`public/${rcFile.name}`, file, {
                          cacheControl: "3600",
                          upsert: true,
                        });
    
                      const { data } = supabaseClient.storage
                        .from("refineflix")
                        .getPublicUrl(`public/${rcFile.name}`);
    
                      const xhr = new XMLHttpRequest();
                      onSuccess && onSuccess({ url: data?.publicURL }, xhr);
                    } catch (error) {
                      onError && onError(new Error("Upload Error"));
                    }
                  }}
                >
                  <p className="ant-upload-text">Drag & drop a file in this area</p>
                </Upload.Dragger>
              </Form.Item>
            </Form.Item>
          </Form>
        </Edit>
      );
    };
    
    



  • 페이지 보기

  • import {
      useShow,
      Show,
      Typography,
      IResourceComponentsProps,
      Space,
      ImageField,
      RefreshButton,
      EditButton,
      useNavigation,
    } from "@pankod/refine";
    
    import { IMovies } from "interfaces";
    
    const { Title, Text } = Typography;
    
    export const AdminMovieShow: React.FC<IResourceComponentsProps> = () => {
      const { queryResult } = useShow<IMovies>();
      const { data, isLoading } = queryResult;
      const record = data?.data;
    
      const { push } = useNavigation();
    
      return (
        <Show
          isLoading={isLoading}
          pageHeaderProps={{
            title: record?.name,
            subTitle: record?.premiere,
            extra: (
              <>
                <EditButton
                  onClick={() => push(`/admin/movies/edit/${record?.id}`)}
                />
                <RefreshButton />
              </>
            ),
          }}
        >
          <Title level={5}>Director</Title>
          <Text>{record?.director || "-"}</Text>
    
          <Title level={5}>Stars</Title>
          <Text>{record?.stars || "-"}</Text>
    
          <Title level={5}>Trailer</Title>
          {record?.trailer && (
            <video width="400" controls>
              <source src={record.trailer} type="video/mp4" />
            </video>
          )}
    
          <Title level={5}>Images</Title>
          <Space wrap>
            {record?.images ? (
              record.images.map((img) => (
                <ImageField
                  key={img.name}
                  value={img.url}
                  title={img.name}
                  width={200}
                />
              ))
            ) : (
              <Text>Not found any images</Text>
            )}
          </Space>
        </Show>
      );
    };
    
    




    최종 버전<Resource> .

          <Resource
            name="movies"
            list={AdminMovieList}
            create={AdminMovieCreate}
            show={AdminMovieShow}
            edit={AdminMovieEdit}
            options={{
              route: "admin/movies",
            }}
          />
    


    3. 영화 목록 페이지 만들기



    권한이 없는 사용자를 위해 사용자 지정 목록을 만들고 페이지를 표시하므로 이러한 페이지에 대한 사용자 지정 경로를 추가해야 합니다.

    App.tsx

    import { Refine, Resource } from "@pankod/refine";
    
    import "@pankod/refine/dist/styles.min.css";
    import { dataProvider } from "@pankod/refine-supabase";
    
    import authProvider from "./authProvider";
    import { supabaseClient } from "utility";
    
    import {
      AdminMovieList,
      AdminMovieCreate,
      AdminMovieShow,
      AdminMovieEdit,
    } from "./pages/admin/movies";
    import { MoviesList, MovieShow } from "./pages/movies";
    import { Login } from "./pages/login";
    
    function App() {
      return (
        <Refine
          dataProvider={dataProvider(supabaseClient)}
          authProvider={authProvider}
          LoginPage={Login}
          routes={[
            {
              exact: true,
              component: MoviesList,
              path: "/movies",
            },
            {
              exact: true,
              component: MovieShow,
              path: "/:resource(movies)/:action(show)/:id",
            },
          ]}
        >
          <Resource
            name="movies"
            list={AdminMovieList}
            create={AdminMovieCreate}
            show={AdminMovieShow}
            edit={AdminMovieEdit}
            options={{
              route: "admin/movies",
            }}
          />
        </Refine>
      );
    }
    
    export default App;
    
    


  • 영화 목록 페이지

  • import {
      IResourceComponentsProps,
      Card,
      Space,
      useList,
      useNavigation,
    } from "@pankod/refine";
    import { Layout } from "components";
    
    import { IMovies } from "interfaces";
    
    export const MoviesList: React.FC<IResourceComponentsProps> = () => {
      const { Meta } = Card;
    
      const { data, isLoading } = useList<IMovies>({
        resource: "movies",
        queryOptions: {
          staleTime: 0,
        },
      });
    
      const { push } = useNavigation();
    
      const renderMovies = () => {
        if (data) {
          return data.data.map((movie) => {
            return (
              <Card
                hoverable
                key={movie.name}
                style={{ width: 240, minHeight: 400 }}
                cover={
                  movie.images?.length > 0 ? (
                    <img alt={movie.images[0].name} src={movie.images[0].url} />
                  ) : (
                    <img
                      alt="default"
                      src="https://cdn.pixabay.com/photo/2019/04/24/21/55/cinema-4153289_960_720.jpg"
                    />
                  )
                }
                loading={isLoading}
                onClick={() => push(`/movies/show/${movie.id}`)}
              >
                <Meta title={movie.name} description={movie.description} />
              </Card>
            );
          });
        }
      };
    
      return (
        <Layout>
          <Space align="start">{renderMovies()}</Space>
        </Layout>
      );
    };
    
    



  • 영화 세부 정보 페이지

  • import {
      useShow,
      Show,
      Typography,
      IResourceComponentsProps,
      Space,
      ImageField,
    } from "@pankod/refine";
    import { Layout } from "components";
    
    import { IMovies } from "interfaces";
    
    const { Title, Text } = Typography;
    
    export const MovieShow: React.FC<IResourceComponentsProps> = () => {
      const { queryResult } = useShow<IMovies>();
      const { data, isLoading } = queryResult;
      const record = data?.data;
    
      const renderDetail = () => (
        <>
          <Title level={5}>Director</Title>
          <Text>{record?.director || "-"}</Text>
    
          <Title level={5}>Stars</Title>
          <Text>{record?.stars || "-"}</Text>
          <Title level={5}>Trailer</Title>
          {record?.trailer && (
            <video width="400" controls>
              <source src={record.trailer} type="video/mp4" />
            </video>
          )}
          <Title level={5}>Images</Title>
          <Space wrap>
            {record?.images ? (
              record.images.map((img) => (
                <ImageField
                  key={img.name}
                  value={img.url}
                  title={img.name}
                  width={200}
                />
              ))
            ) : (
              <Text>Not found any images</Text>
            )}
          </Space>
        </>
      );
    
      return (
        <Layout>
          <Show
            isLoading={isLoading}
            pageHeaderProps={{
              title: record?.name,
              subTitle: record?.premiere,
              extra: null,
            }}
          >
            {renderDetail()}
          </Show>
        </Layout>
      );
    };
    
    




    here is repo

    좋은 웹페이지 즐겨찾기