ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,87 @@
+import { graphql, useFragment, useMutation } from "react-relay";
+
+import { RepositoryItem_repository$key } from "./__generated__/RepositoryItem_repository.graphql";
+import { RepositoryItemAddStarMutation } from "./__generated__/RepositoryItemAddStarMutation.graphql";
+import { RepositoryItemRemoveStarMutation } from "./__generated__/RepositoryItemRemoveStarMutation.graphql";
+
+import { StarButton } from "./StarButton";
+
+type RepositoryItemProps = {
+  repository: RepositoryItem_repository$key;
+};
+
+const RepositoryStarAddMutation = graphql`
+  mutation RepositoryItemAddStarMutation($id: ID!) {
+    addStar(input: { starrableId: $id }) {
+      starrable {
+        id
+        viewerHasStarred
+      }
+    }
+  }
+`;
+
+const RepositoryStarRemoveMutation = graphql`
+  mutation RepositoryItemRemoveStarMutation($id: ID!) {
+    removeStar(input: { starrableId: $id }) {
+      starrable {
+        id
+        viewerHasStarred
+      }
+    }
+  }
+`;
+
+export const RepositoryItem = (props: RepositoryItemProps) => {
+  const payload = useFragment(
+    graphql`
+      fragment RepositoryItem_repository on Repository {
+        id
+        name
+        url
+        description
+        stargazerCount
+        viewerHasStarred
+      }
+    `,
+    props.repository
+  );
+
+  const [add, isAdding] = useMutation<RepositoryItemAddStarMutation>(
+    RepositoryStarAddMutation
+  );
+
+  const [remove, isRemoving] = useMutation<RepositoryItemRemoveStarMutation>(
+    RepositoryStarRemoveMutation
+  );
+
+  if (!payload) {
+    return null;
+  }
+
+  const { id, url, name, description, stargazerCount, viewerHasStarred } =
+    payload;
+
+  const isLoading = isAdding || isRemoving;
+
+  return (
+    <li>
+      <a href={url} target="_blank" rel="noreferrer">
+        {name}
+      </a>
+      <p>{description}</p>
+      <p>⭐️ {stargazerCount}</p>
+
+      {isLoading ? (
+        <div>...</div>
+      ) : (
+        <StarButton
+          repositoryId={id}
+          isStarred={viewerHasStarred}
+          onAddStar={() => add({ variables: { id } })}
+          onRemoveStar={() => remove({ variables: { id } })}
+        />
+      )}
+    </li>
+  );
+};

리뷰: TODO: add, remove에 대한 변수값은 StarButton 내에서 넘겨받도록 변경
@@ -0,0 +1,77 @@
+import React, { useState } from "react";
+import styled from "styled-components";
+import { Star as StarIcon } from "lucide-react";
+
+interface StarRatingProps {
+  totalStars?: number;
+  initialRating?: number;
+  onRatingChange?: (rating: number) => void;
+}
+
+const BackgroundContainer = styled.div`
+  background-color: #f7f7f7;
+  width: 807.73px;
+  height: 251.63px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+`;
+
+const StarContainer = styled.div`
+  display: flex;
+  gap: 4px; 
+`;
+
+const StyledStar = styled.div<{ filled: boolean }>`
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  svg {
+    width: 94.96px; 
+    height: 99.25px;
+    color: ${(props) => (props.filled ? "#facc15" : "#d1d5db")};
+  }
+`;
+
+const StarRating: React.FC<StarRatingProps> = ({
+  totalStars = 5,
+  initialRating = 0,
+  onRatingChange,
+}) => {
+  const [rating, setRating] = useState(initialRating);
+
+  const handleRating = (index: number) => {
+    let newRating = rating;
+    if (index + 1 === rating) {
+      newRating = index;
+    } else if (index < rating) {
+      newRating = index;
+    } else {
+      newRating = index + 1;
+    }
+
+    setRating(newRating);
+    if (onRatingChange) {
+      onRatingChange(newRating);
+    }
+  };
+
+  return (
+    <BackgroundContainer>
+      <StarContainer>
+        {Array.from({ length: totalStars }, (_, index) => (
+          <StyledStar
+            key={index}
+            filled={index < rating}
+            onClick={() => handleRating(index)}
+          >
+            <StarIcon fill={index < rating ? "#facc15" : "none"} stroke="#facc15" />
+          </StyledStar>
+        ))}
+      </StarContainer>
+    </BackgroundContainer>
+  );
+};
+
+export default StarRating;

리뷰: filled 안쪽에 String으로 감싸주세요
@@ -0,0 +1,77 @@
+import React, { useState } from "react";
+import styled from "styled-components";
+import { Star as StarIcon } from "lucide-react";
+
+interface StarRatingProps {
+  totalStars?: number;
+  initialRating?: number;
+  onRatingChange?: (rating: number) => void;
+}
+
+const BackgroundContainer = styled.div`
+  background-color: #f7f7f7;
+  width: 807.73px;
+  height: 251.63px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+`;
+
+const StarContainer = styled.div`
+  display: flex;
+  gap: 4px; 
+`;
+
+const StyledStar = styled.div<{ filled: boolean }>`
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  svg {
+    width: 94.96px; 
+    height: 99.25px;
+    color: ${(props) => (props.filled ? "#facc15" : "#d1d5db")};
+  }
+`;
+
+const StarRating: React.FC<StarRatingProps> = ({
+  totalStars = 5,
+  initialRating = 0,
+  onRatingChange,
+}) => {
+  const [rating, setRating] = useState(initialRating);
+
+  const handleRating = (index: number) => {
+    let newRating = rating;
+    if (index + 1 === rating) {
+      newRating = index;
+    } else if (index < rating) {
+      newRating = index;
+    } else {
+      newRating = index + 1;
+    }
+
+    setRating(newRating);
+    if (onRatingChange) {
+      onRatingChange(newRating);
+    }
+  };
+
+  return (
+    <BackgroundContainer>
+      <StarContainer>
+        {Array.from({ length: totalStars }, (_, index) => (
+          <StyledStar
+            key={index}
+            filled={index < rating}
+            onClick={() => handleRating(index)}
+          >
+            <StarIcon fill={index < rating ? "#facc15" : "none"} stroke="#facc15" />
+          </StyledStar>
+        ))}
+      </StarContainer>
+    </BackgroundContainer>
+  );
+};
+
+export default StarRating;

리뷰: 제가 후기 페이지 만들면서 이 부분 수정해서 한꺼번에 PR 올리겠습니다
@@ -0,0 +1,67 @@
+import { Button, Form, Input, Select, Space } from 'antd';
+import { REVIEW_CYCLE_FORM_NAMES, REVIEW_CYCLE_RULES } from './constants';
+import useMembers from '@apis/review/useMembers';
+import { ReviewCycleRequest } from '@apis/review/type';
+import { ReviewCycle } from '@apis/review/entities';
+
+interface Props {
+  onFinish: (values: ReviewCycleRequest) => void;
+  onReset: () => void;
+  initialValues?: {
+    name: ReviewCycle['name'];
+    reviewees: number[];
+    title: ReviewCycle['question']['title'];
+    description: ReviewCycle['question']['description'];
+  };
+  confirmButtonProps?: { text: string };
+  deleteButtonProps?: { text: string; handleClick: () => void };
+}
+
+export default function ReviewCycleForm({
+  onFinish,
+  onReset,
+  initialValues,
+  confirmButtonProps = { text: '확인' },
+  deleteButtonProps,
+}: Props) {
+  const { members, isLoading } = useMembers();
+
+  return (
+    <Form labelAlign="right" onFinish={onFinish} onReset={onReset} initialValues={initialValues}>
+      <Form.Item label="리뷰 정책 이름" name={REVIEW_CYCLE_FORM_NAMES.name} rules={REVIEW_CYCLE_RULES.reviewCycleName}>
+        <Input />
+      </Form.Item>
+
+      <Form.Item label="리뷰 받는 사람" name={REVIEW_CYCLE_FORM_NAMES.reviewees} rules={REVIEW_CYCLE_RULES.reviewees}>
+        <Select
+          mode="multiple"
+          loading={isLoading}
+          placeholder="리뷰 받는 사람을 선택해주세요."
+          optionFilterProp="label"
+          options={members?.map(m => ({ key: m.entityId, label: m.name, value: m.entityId }))}
+        />
+      </Form.Item>
+
+      <Form.Item label="질문" name={REVIEW_CYCLE_FORM_NAMES.question.title} rules={REVIEW_CYCLE_RULES.questionTitle}>
+        <Input />
+      </Form.Item>
+
+      <Form.Item
+        label="질문 설명"
+        name={REVIEW_CYCLE_FORM_NAMES.question.description}
+        rules={REVIEW_CYCLE_RULES.questionDescription}
+      >
+        <Input />
+      </Form.Item>
+      <Form.Item style={{ textAlign: 'right' }}>
+        <Space size={10}>
+          <Button htmlType="reset">취소</Button>
+          {deleteButtonProps && <Button onClick={deleteButtonProps.handleClick}>{deleteButtonProps.text}</Button>}
+          <Button type="primary" htmlType="submit">
+            {confirmButtonProps.text}
+          </Button>
+        </Space>
+      </Form.Item>
+    </Form>
+  );
+}

리뷰: 요기 `form`은 없어도 될 것 같아요~
@@ -0,0 +1,67 @@
+import { Button, Form, Input, Select, Space } from 'antd';
+import { REVIEW_CYCLE_FORM_NAMES, REVIEW_CYCLE_RULES } from './constants';
+import useMembers from '@apis/review/useMembers';
+import { ReviewCycleRequest } from '@apis/review/type';
+import { ReviewCycle } from '@apis/review/entities';
+
+interface Props {
+  onFinish: (values: ReviewCycleRequest) => void;
+  onReset: () => void;
+  initialValues?: {
+    name: ReviewCycle['name'];
+    reviewees: number[];
+    title: ReviewCycle['question']['title'];
+    description: ReviewCycle['question']['description'];
+  };
+  confirmButtonProps?: { text: string };
+  deleteButtonProps?: { text: string; handleClick: () => void };
+}
+
+export default function ReviewCycleForm({
+  onFinish,
+  onReset,
+  initialValues,
+  confirmButtonProps = { text: '확인' },
+  deleteButtonProps,
+}: Props) {
+  const { members, isLoading } = useMembers();
+
+  return (
+    <Form labelAlign="right" onFinish={onFinish} onReset={onReset} initialValues={initialValues}>
+      <Form.Item label="리뷰 정책 이름" name={REVIEW_CYCLE_FORM_NAMES.name} rules={REVIEW_CYCLE_RULES.reviewCycleName}>
+        <Input />
+      </Form.Item>
+
+      <Form.Item label="리뷰 받는 사람" name={REVIEW_CYCLE_FORM_NAMES.reviewees} rules={REVIEW_CYCLE_RULES.reviewees}>
+        <Select
+          mode="multiple"
+          loading={isLoading}
+          placeholder="리뷰 받는 사람을 선택해주세요."
+          optionFilterProp="label"
+          options={members?.map(m => ({ key: m.entityId, label: m.name, value: m.entityId }))}
+        />
+      </Form.Item>
+
+      <Form.Item label="질문" name={REVIEW_CYCLE_FORM_NAMES.question.title} rules={REVIEW_CYCLE_RULES.questionTitle}>
+        <Input />
+      </Form.Item>
+
+      <Form.Item
+        label="질문 설명"
+        name={REVIEW_CYCLE_FORM_NAMES.question.description}
+        rules={REVIEW_CYCLE_RULES.questionDescription}
+      >
+        <Input />
+      </Form.Item>
+      <Form.Item style={{ textAlign: 'right' }}>
+        <Space size={10}>
+          <Button htmlType="reset">취소</Button>
+          {deleteButtonProps && <Button onClick={deleteButtonProps.handleClick}>{deleteButtonProps.text}</Button>}
+          <Button type="primary" htmlType="submit">
+            {confirmButtonProps.text}
+          </Button>
+        </Space>
+      </Form.Item>
+    </Form>
+  );
+}

리뷰: 오 이런 방식으로 타이핑 하면 `initialValues`에 대한 타입을 따로 만들어주지 않아도 되고 좋네요 👍
@@ -0,0 +1,17 @@
+import { Rule } from 'antd/lib/form';
+
+export const REVIEW_CYCLE_FORM_NAMES = {
+  name: 'name',
+  reviewees: 'reviewees',
+  question: {
+    title: 'title',
+    description: 'description',
+  },
+} as const;
+
+export const REVIEW_CYCLE_RULES: Record<string, Rule[]> = {
+  reviewCycleName: [{ required: true, message: '리뷰 정책 이름을 입력하세요.' }],
+  reviewees: [{ required: true, message: '리뷰 받는 사람을 선택하세요.' }],
+  questionTitle: [{ required: true, message: '질문 제목을 입력하세요.' }],
+  questionDescription: [{ required: true, message: '질문 설명을 입력하세요.' }],
+};

리뷰: 요기 상수는 따로 빼신 이유가 뭘까요? 
`Form.Item`의 `name` 외에는 사용되지 않는 것 같은데 `label`처럼 바로 넣어도 좋아보여요!
아니면 label, name, rule을 하나로 묶어도 좋을 것 같구요! 가볍게 드는 생각 남겨봅니다~
@@ -0,0 +1,90 @@
+import { API_PATH } from '@apis/constants';
+import { useQuery } from '@apis/common/useQuery';
+import { ReviewCycle } from './entities';
+import { ReviewCycleRequest, ReviewCycleResponse } from './type';
+import { useDelete, usePost, usePut } from '@apis/common/useMutation';
+import { get } from 'lodash';
+
+export function useGetReviewCycles() {
+  const {
+    data,
+    isLoading,
+    mutate: refetchReviewCycle,
+  } = useQuery<{ reviewCycles: ReviewCycle[] }>(API_PATH.REVIEW_CYCLES);
+
+  return {
+    reviewCycles: data?.reviewCycles,
+    isLoading,
+    refetchReviewCycle,
+  };
+}
+
+export function useCreateReviewCycle({
+  onSuccess,
+  onError,
+}: {
+  onSuccess: () => void;
+  onError: (message: string) => void;
+}) {
+  const { trigger, isMutating } = usePost<ReviewCycleRequest, ReviewCycleResponse>({
+    endpoint: API_PATH.REVIEW_CYCLES,
+    onSuccess: onSuccess,
+    onError: err => {
+      const errorMessage = get(err, 'response.data.error.message', '리뷰 사이클 생성에 실패했습니다.');
+      onError(errorMessage);
+    },
+  });
+
+  return {
+    createReviewCycle: (reviewCycle: ReviewCycleRequest) => trigger(reviewCycle),
+    isMutating,
+  };
+}
+
+export function useUpdateReviewCycle({
+  entityId,
+  onSuccess,
+  onError,
+}: {
+  entityId?: number;
+  onSuccess: () => void;
+  onError: (message: string) => void;
+}) {
+  const { trigger, isMutating } = usePut<ReviewCycleRequest, ReviewCycleResponse>({
+    endpoint: entityId ? `${API_PATH.REVIEW_CYCLES}/${entityId}` : null,
+    onSuccess: onSuccess,
+    onError: err => {
+      const errorMessage = get(err, 'response.data.error.message', '리뷰 사이클 수정에 실패했습니다.');
+      onError(errorMessage);
+    },
+  });
+
+  return {
+    updateReviewCycle: (reviewCycle: ReviewCycleRequest) => trigger(reviewCycle),
+    isMutating,
+  };
+}
+
+export function useDeleteReviewCycle({
+  entityId,
+  onSuccess,
+  onError,
+}: {
+  entityId: number;
+  onSuccess: () => void;
+  onError: (message: string) => void;
+}) {
+  const { trigger, isMutating } = useDelete({
+    endpoint: `${API_PATH.REVIEW_CYCLES}/${entityId}`,
+    onSuccess,
+    onError: err => {
+      const errorMessage = get(err, 'response.data.error.message', '리뷰 사이클 삭제에 실패했습니다.');
+      onError(errorMessage);
+    },
+  });
+
+  return {
+    deleteReviewCycle: trigger,
+    isMutating,
+  };
+}

리뷰: [질문]
`trigger`를 바로 넘기지 않고 함수로 넘겨주는 이유는 타입을 고정시키기 위함일까요?
@@ -0,0 +1,37 @@
+import useModal from 'hooks/useModal';
+import { Headline4 } from 'styles/typography';
+import ReviewCycleForm from '../ReviewCycleForm';
+import { message } from 'antd';
+import { useCreateReviewCycle, useGetReviewCycles } from '@apis/review/useReviewCycles';
+
+function useReviewCycleCreateModal() {
+  const modal = useModal();
+
+  const { refetchReviewCycle } = useGetReviewCycles();
+  const { createReviewCycle } = useCreateReviewCycle({
+    onSuccess: () => {
+      message.success('새로운 리뷰 정책을 생성했어요.');
+      modal.close();
+      refetchReviewCycle();
+    },
+    onError: message.error,
+  });
+
+  function render() {
+    return modal.render({
+      title: <Headline4>새로운 리뷰 정책 만들기</Headline4>,
+      visible: modal.visible,
+      children: (
+        <ReviewCycleForm onFinish={createReviewCycle} onReset={modal.close} confirmButtonProps={{ text: '생성하기' }} />
+      ),
+    });
+  }
+
+  return {
+    render,
+    open: modal.open,
+    close: modal.close,
+  };
+}
+
+export default useReviewCycleCreateModal;

리뷰: 요런 식으로 기존 `modal`을 확장하는 훅으로 만들어도 좋을 것 같아요~
```suggestion
  return {
    ...modal,
    render,
  };
```
@@ -0,0 +1,25 @@
+import { DeleteOutlined } from '@ant-design/icons';
+import { useDeleteReviewCycle, useGetReviewCycles } from '@apis/review/useReviewCycles';
+import { message } from 'antd';
+
+function DeleteButton({ entityId }: { entityId: number }) {
+  const { refetchReviewCycle } = useGetReviewCycles();
+  const { deleteReviewCycle } = useDeleteReviewCycle({
+    entityId,
+    onSuccess: refetchReviewCycle,
+    onError: message.error,
+  });
+
+  return (
+    <button
+      onClick={e => {
+        e.stopPropagation();
+        deleteReviewCycle();
+      }}
+    >
+      <DeleteOutlined />
+    </button>
+  );
+}
+
+export default DeleteButton;

리뷰: [질문]
`e.stopPropagation()`의 의도가 무엇인가요?
@@ -0,0 +1,10 @@
+import { Member, ReviewCycle } from './entities';
+
+export type ReviewCycleRequest = {
+  name: ReviewCycle['name'];
+  reviewees: Member['entityId'][];
+  title: string;
+  description: string;
+};
+
+export type ReviewCycleResponse = Pick<ReviewCycle, 'entityId'>;

리뷰: [제안]
이것과 entities의 타입을 일부 공통으로 사용해볼 수는 없으려나요? 같은 맥락을 담고 있는 타입인데 맥락이 아예 분리되어서 나중에 유지보수할 때 주의해야겠다는 생각이 드네요
@@ -0,0 +1,67 @@
+import { Button, Form, Input, Select, Space } from 'antd';
+import { REVIEW_CYCLE_FORM_NAMES, REVIEW_CYCLE_RULES } from './constants';
+import useMembers from '@apis/review/useMembers';
+import { ReviewCycleRequest } from '@apis/review/type';
+import { ReviewCycle } from '@apis/review/entities';
+
+interface Props {
+  onFinish: (values: ReviewCycleRequest) => void;
+  onReset: () => void;
+  initialValues?: {
+    name: ReviewCycle['name'];
+    reviewees: number[];
+    title: ReviewCycle['question']['title'];
+    description: ReviewCycle['question']['description'];
+  };
+  confirmButtonProps?: { text: string };
+  deleteButtonProps?: { text: string; handleClick: () => void };
+}
+
+export default function ReviewCycleForm({
+  onFinish,
+  onReset,
+  initialValues,
+  confirmButtonProps = { text: '확인' },
+  deleteButtonProps,
+}: Props) {
+  const { members, isLoading } = useMembers();
+
+  return (
+    <Form labelAlign="right" onFinish={onFinish} onReset={onReset} initialValues={initialValues}>
+      <Form.Item label="리뷰 정책 이름" name={REVIEW_CYCLE_FORM_NAMES.name} rules={REVIEW_CYCLE_RULES.reviewCycleName}>
+        <Input />
+      </Form.Item>
+
+      <Form.Item label="리뷰 받는 사람" name={REVIEW_CYCLE_FORM_NAMES.reviewees} rules={REVIEW_CYCLE_RULES.reviewees}>
+        <Select
+          mode="multiple"
+          loading={isLoading}
+          placeholder="리뷰 받는 사람을 선택해주세요."
+          optionFilterProp="label"
+          options={members?.map(m => ({ key: m.entityId, label: m.name, value: m.entityId }))}
+        />
+      </Form.Item>
+
+      <Form.Item label="질문" name={REVIEW_CYCLE_FORM_NAMES.question.title} rules={REVIEW_CYCLE_RULES.questionTitle}>
+        <Input />
+      </Form.Item>
+
+      <Form.Item
+        label="질문 설명"
+        name={REVIEW_CYCLE_FORM_NAMES.question.description}
+        rules={REVIEW_CYCLE_RULES.questionDescription}
+      >
+        <Input />
+      </Form.Item>
+      <Form.Item style={{ textAlign: 'right' }}>
+        <Space size={10}>
+          <Button htmlType="reset">취소</Button>
+          {deleteButtonProps && <Button onClick={deleteButtonProps.handleClick}>{deleteButtonProps.text}</Button>}
+          <Button type="primary" htmlType="submit">
+            {confirmButtonProps.text}
+          </Button>
+        </Space>
+      </Form.Item>
+    </Form>
+  );
+}

리뷰: [적극]
ui를 렌더링하기 위한 buttonProps가 객체형태로 내려가면 나중에 유지보수차원에서 어려움이 있을 것 같은데 방법이 없으려나요??
confirmButtonProps나 deleteButtonProps는 Form이라는 맥락에서는 안에 들어가도 될 것 같다고 판단할 수도 있으나 실제 렌더하는 쪽에서는 prop을 꺼내쓰는 차원에 불과해서 어떻게든 분리가 되면 좋겠다는 생각이 들어요.
@@ -0,0 +1,28 @@
+import { faker } from '@faker-js/faker';
+import { personList } from '@mocks/auth/data';
+import { v4 as uuidv4 } from 'uuid';
+
+const personCount = personList.length;
+
+export const reviewCycleList = Array.from({ length: 10 }, (_, i) => ({
+  entityId: uuidv4(),
+  name: faker.lorem.words(),
+  creator: personList[Math.floor(Math.random() * personCount)],
+  reviewees: shuffleArray(personList).slice(0, Math.floor(Math.random() * personCount) + 1),
+  question: {
+    title: faker.lorem.words(),
+    description: faker.lorem.sentence(),
+  },
+  updatedAt: faker.date.past().toISOString(),
+}));
+
+function shuffleArray<T>(array: T[]) {
+  const result = [...array];
+
+  for (let i = result.length - 1; i > 0; i--) {
+    const j = Math.floor(Math.random() * (i + 1));
+    [result[i], result[j]] = [result[j], result[i]];
+  }
+
+  return result;
+}

리뷰: `faker` 라이브러리 적절히 잘 쓰시네요 👍
@@ -0,0 +1,25 @@
+import { DeleteOutlined } from '@ant-design/icons';
+import { useDeleteReviewCycle, useGetReviewCycles } from '@apis/review/useReviewCycles';
+import { message } from 'antd';
+
+function DeleteButton({ entityId }: { entityId: number }) {
+  const { refetchReviewCycle } = useGetReviewCycles();
+  const { deleteReviewCycle } = useDeleteReviewCycle({
+    entityId,
+    onSuccess: refetchReviewCycle,
+    onError: message.error,
+  });
+
+  return (
+    <button
+      onClick={e => {
+        e.stopPropagation();
+        deleteReviewCycle();
+      }}
+    >
+      <DeleteOutlined />
+    </button>
+  );
+}
+
+export default DeleteButton;

리뷰: [적극] 
버튼인데 버튼이 아닌 컴포넌트가 된 것 같아요.
아이콘에 클릭이벤트를 다는 것에 대해서는 어떻게 생각하시나용? role=button이 되도록 해야하나 하는 판단이 드네요
@@ -0,0 +1,43 @@
+import { Button } from 'antd';
+import { RoundContent } from 'components/common/RoundContent';
+import useReviewCycleCreateModal from './ReviewCycleModals/useReviewCycleCreateModal';
+import ReviewCycleTable from './ReviewCycleTable';
+import useReviewCycleUpdateModal from './ReviewCycleModals/useReviewCycleUpdateModal';
+import useSelectedReviewCycle from './useSelectedReviewCycle';
+
+function ReviewCycles() {
+  const { selectedReviewCycle } = useSelectedReviewCycle();
+
+  const reviewCycleCreateModal = useReviewCycleCreateModal();
+  const reviewCycleUpdateModal = useReviewCycleUpdateModal({
+    entityId: selectedReviewCycle?.entityId,
+  });
+
+  function openReviewCycleCreateModal() {
+    reviewCycleCreateModal.open();
+  }
+
+  function openReviewCycleUpdateModal() {
+    reviewCycleUpdateModal.open();
+  }
+
+  return (
+    <RoundContent>
+      <RoundContent.Header
+        title="리뷰 정책 목록"
+        rightArea={
+          <Button type="primary" size="large" onClick={openReviewCycleCreateModal}>
+            리뷰 정책 생성
+          </Button>
+        }
+      />
+      <RoundContent.Body>
+        <ReviewCycleTable onRowClick={openReviewCycleUpdateModal} />
+      </RoundContent.Body>
+      {reviewCycleCreateModal.render()}
+      {reviewCycleUpdateModal.render()}
+    </RoundContent>
+  );
+}
+
+export default ReviewCycles;

리뷰: [적극]
인스턴스는 camelcase가 되면 좋겠습니다!
@@ -0,0 +1,10 @@
+import { Member, ReviewCycle } from './entities';
+
+export type ReviewCycleRequest = {
+  name: ReviewCycle['name'];
+  reviewees: Member['entityId'][];
+  title: string;
+  description: string;
+};
+
+export type ReviewCycleResponse = Pick<ReviewCycle, 'entityId'>;

리뷰: 음 아래처럼 타이핑하면 같은 맥락임이 잘 드러나려나요?

```ts
export type ReviewCycleRequest = {
  name: ReviewCycle['name'];
  reviewees: Member['entityId'][];
  title: string;
  description: string;
};

export type ReviewCycleResponse = Pick<ReviewCycle, 'entityId'>;
```