태그 상세 페이지 개발 (23.11.07)

#현업

이번 스프린트에 포스트 리스트와 커뮤니티 리스트, 크리에이터 리스트를 모아서 보여주는 페이지 개발을 하게되었다.

단순하게 생각하면 리스트만 뿌리면 끝 이지만, 이번엔 원래 하던대로 하지 않고 조금 더 생각하고 리팩토링 해 볼 각오로 업무를 착수했다.

처음에는 리팩토링부터 시작했다.

현재 포스트 리스트만 노출시키는 ui 컴포넌트 를 포스트, 커뮤니티 데이터를 노출시켜야 했다.

이를 같이 사용하려고 보니 포스트 리스트의 데이터 타입과 커뮤니티 리스트 데이터 타입은 달랐다.

export type PostDetail = {
  postUuid: string;
  state: PostState;
  visible: VisibleType;
  title: string;
  previewText: string;
  body: string;
  tags: string[];
  memberships: MembershipItem[];
  attachedFiles: AttachedFile[];
  attachedAudio: AttachedAudio;
  likedCountCache: number;
  postCommentsCount: number;
  previewImageUrls: string[];
  viewCountCache: number;
  audioPlayCount: number;
  createdAt: string;
  displayDateTime: string;
  publishedAt: string;
  updatedAt: string;
  profile: {
    creatorPermalink: string;
    profileImageUrl: string;
    userName: string;
    userUuid: string;
    shortDescription: string;
    categoryName: string;
  };
  locked: boolean;
  prevPostUuid: string | null;
  nextPostUuid: string | null;
  category: PostCategoryType;
  isSale: boolean;
  pointAmount: number;
  bodyTextCount: number;
  imageCount: number;
  attachedFileCount: number;
  audioPlayTime: string;
  contentType: PointPaymentContentType;
  soldCountCache: number;
};
export type CommunityDetail = {
  visible: COMMUNITY_OPEN_SCOPES;
  membership?: MembershipSimpleInfo;
  imgFilePaths: string[];
  body: string;
  createdAt: string;
  displayDateTime: string;
  postUuid: string;
  updatedAt: string;
  locked: boolean;
  writerInfo: {
    creatorPermalink: string;
    profileImageUrl: string;
    userName: string;
    userType: WriterType;
    userUuid: string;
  };
  creatorProfile: {
    creatorPermalink: string;
    profileImageUrl: string;
    userName: string;
    userType: WriterType;
    userUuid: string;
  };
  likedCountCache: number;
  postCommentsCache: number;
  viewCountCache: number;
};

일단 현재 포스트 리스트만 노출시키는 컴포넌트를 공통적으로 사용할 수 있도록 변경했다.

이 부분에서 고민이 됐다.

지금까지의 작업은 포스트 리스트만 노출시키는 컴포넌트를 공통으로 사용할 수 있도록 만들었다.

기존에 포스트 리스트만 노출시킬 수 있는 컴포넌트는 그대로 두고,

커뮤니티 리스트를 노출시킬 수 있는 컴포넌트를 새로 만들어 postType에 따라 다른 컴포넌트를 렌더할 수 있도록 하는건 어떨까?

왜냐하면 포스트와 커뮤니티의 차이점이 있는데, 포스트는 title, pointAmount가 있고 커뮤니티는 title, pointAmount가 없다.

그에따라 보여지는 ui도 조금 차이가 있다.

이는 해당 컴포넌트 하나로 처리하게되면 내부에서 props 존재 여부에 따라 렌더링 조건문이 추가된다.

이런 상황을 고려해보았을 때 추후에 postListComponent 에 ui 요소가 하나씩 추가될때마다 컴포넌트 내부에서 처리하는 조건문이 많아질 것이고 이는 컴포넌트의 역할을 생각했을때 더 많은 역할을 하게될 수 있을것 같다고 판단했다.

postListComponent를 리팩토링하고, communityPostListComponent를 새로 추가하는것으로 결정했다.

AS-IS PostCard.tsx

function PostCard({
  post,
  sort,
  screenSection,
  isOpenNewTab,
  highlightKeyword,
}: PostCardProps) {
  const {
    postUuid,
    body,
    previewImageUrls,
    profile,
    displayDateTime,
    title,
    visible,
    locked,
    attachedAudio,
    viewCountCache,
    likedCountCache,
    postCommentsCount,
    isSale,
    pointAmount,
  } = post;
  const { userProfile } = useUserProfile();
  const permalink = profile.creatorPermalink;
  const [isDesktopView, setIsDesktopView] = useState<boolean>(false);

  const setViewport = () => {
    const viewportWidth = window.innerWidth;
    setIsDesktopView(viewportWidth > 768);
  };

  const titleLineClamp = !body ? 'text-ellipsis-2' : 'text-ellipsis-1';
  const bodyLineClamp =
    // m : 멤버공개 포스트 1줄 말줄임 / 전체공개 포스트 2줄 말줄임
    // pc : 멤버공개, 전체공개 포스트 2줄 말줄임
    isDesktopView || visible === 'MEMBER'
      ? 'text-ellipsis-1'
      : 'text-ellipsis-2';

  const hasThumbnail = previewImageUrls?.length > 0;

  const onCardClick = () => {
    isOpenNewTab
      ? window.open(`/creator/${permalink}/posts/${postUuid}`, '_blank')
      : Router.push(`/creator/${permalink}/posts/${postUuid}`);
  };

  useEffect(() => {
    setViewport();
    window.addEventListener('resize', setViewport);

    return () => {
      window.removeEventListener('resize', setViewport);
    };
  }, []);

  const showUtilVisibleInfo = isSale || visible !== 'PUBLIC';

  return (
    <DataLogging eventName="Clicked Creator Post" trackData={loggingData}>
      <button
        type="button"
        aria-label="post card"
        className="block text-left w-full"
        onClick={onCardClick}
      >
        <div className="flex gap-x-[16px]">
          <div className="w-full h-[73px] xl:h-auto break-all">
            <h2
              className={`overflow-hidden font_title_bold_md xl:font_title_bold_lg content_secondary ${titleLineClamp}`}
            >
              {!!attachedAudio && (
                <AudioSolid className="inline-block mr-[7px] shrink-0 w-[20px] h-[20px] content_accent" />
              )}
              {highlightKeyword ? (
                <HighlightText
                  originText={title}
                  highlightKeyword={highlightKeyword}
                />
              ) : (
                title
              )}
            </h2>
            <p
              className={`mt-[4px] mb-[18px] xl:mt-[6px] overflow-hidden font_label_regular_lg 
              content_quaternary ${bodyLineClamp} xl:text-ellipsis-2`}
            >
              {body}
            </p>
          </div>
          {hasThumbnail && (
            <div className="shrink-0 relative w-[72px] md:w-[90px] h-[72px] md:h-[90px] rounded-[8px]">
              <Image
                src={previewImageUrls[0]}
                alt={title}
                className="rounded-[8px]"
                fill
                blurDataURL={previewImageUrls[0]}
                placeholder="blur"
                sizes="(max-width: 500px) 72px, 90px"
              />
              <div className="absolute top-0 left-0 w-full h-full border border_black_opacity rounded-[8px]" />
            </div>
          )}
        </div>
        <div>
          <PostWriteInfo
            writerName={profile.userName}
            publishDateTime={displayDateTime}
          />
          <PostResponseInfo
            isSale={isSale}
            view={viewCountCache}
            comment={postCommentsCount}
            like={likedCountCache}
            point={pointAmount}
            visible={visible}
            isPublic={showUtilVisibleInfo}
          />
        </div>
      </button>
    </DataLogging>
  );
}

TO-BE PostListItem.tsx

interface PostListProps {
  onClickCard: () => void;
  hasAudio: boolean;
  highlightKeyword?: string;
  title?: string;
  body: string;
  isTitleLineBreak?: boolean;
  isBodyLineBreak?: boolean;
  thumbnailImageUrl?: string | undefined;
  writerName: string;
  displayDateTime?: string;
  isSale?: boolean;
  viewCountCache: number;
  commentsCount: number;
  likedCountCache: number;
  pointAmount?: number | undefined;
  visible: VisibleType;
  isVisibleUtilInfo?: boolean;
}

export function PostList({
  onClickCard,
  hasAudio,
  highlightKeyword,
  title,
  body,
  isTitleLineBreak,
  isBodyLineBreak,
  thumbnailImageUrl,
  writerName,
  displayDateTime,
  isSale,
  viewCountCache,
  commentsCount,
  likedCountCache,
  pointAmount,
  visible,
  isVisibleUtilInfo,
}: PostListProps) {
  return (
    <button
      type="button"
      aria-label="post card"
      className="block text-left w-full"
      onClick={onClickCard}
    >
      <div className="flex gap-x-[16px]">
        <div className="w-full h-[73px] xl:h-auto break-all">
          <h2
            className={`overflow-hidden font_title_bold_md xl:font_title_bold_lg content_secondary ${
              isTitleLineBreak ? 'text-ellipsis-2' : 'text-ellipsis-1'
            }`}
          >
            {hasAudio && (
              <AudioSolid className="inline-block mr-[7px] shrink-0 w-[20px] h-[20px] content_accent" />
            )}
            {highlightKeyword ? (
              <HighlightText
                originText={title || ''}
                highlightKeyword={highlightKeyword}
              />
            ) : (
              title
            )}
          </h2>
          <p
            className={`mt-[4px] mb-[18px] xl:mt-[6px] overflow-hidden font_label_regular_lg
              content_quaternary ${
                isBodyLineBreak ? 'text-ellipsis-2' : 'text-ellipsis-1'
              } xl:text-ellipsis-2`}
          >
            {body}
          </p>
        </div>
        {thumbnailImageUrl && (
          <div className="shrink-0 relative w-[72px] md:w-[90px] h-[72px] md:h-[90px] rounded-[8px]">
            <Image
              src={thumbnailImageUrl}
              alt={title || ''}
              className="rounded-[8px]"
              fill
              sizes="(max-width: 500px) 72px, 90px"
            />
            <div className="absolute top-0 left-0 w-full h-full border border_black_opacity rounded-[8px]" />
          </div>
        )}
      </div>
      <div>
        <PostWriteInfo
          writerName={writerName}
          publishDateTime={displayDateTime}
        />
        <PostResponseInfo
          isSale={isSale}
          view={viewCountCache}
          comment={commentsCount}
          like={likedCountCache}
          point={pointAmount}
          visible={visible}
          isPublic={isVisibleUtilInfo}
        />
      </div>
    </button>
  );
}

Last updated