Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

프로젝트와 작업 통계: useProjects

useProjects는 AdvanceTodoProject 객체의 첫 페이지와 프로젝트별 작업 통계를 읽습니다. 연결된 작업을 모두 브라우저로 가져오지 않고 서버에서 종류별 개수를 집계한 뒤, 브라우저에서 코딩 작업과 학습 작업의 개수를 더합니다.

입력과 의존성

훅의 인수는 없습니다. 루트 OsdkProvider가 제공하는 클라이언트와 @advanced-to-do-application/sdk의 생성 유형을 사용합니다. 프로젝트에는 codingTasks, learningTasks 링크가 있어야 하며 연결된 작업의 status 값은 다음 문자열을 사용합니다.

상태실제 값
완료COMPLETED
진행 중IN PROGRESS
시작 전NOT STARTED

API 이름이나 상태 값을 바꾸면 해당 집계 조건도 바꿔야 합니다. 작업 유형을 추가해도 그 유형의 개수가 자동으로 이 통계에 포함되지는 않습니다.

상태 값을 여러 곳에서 참조한다면 다음처럼 상수로 모아 오타와 불일치를 줄일 수 있습니다.

const TASK_STATUS = {
  COMPLETED: "COMPLETED",
  IN_PROGRESS: "IN PROGRESS",
  NOT_STARTED: "NOT STARTED"
} as const;

조회와 집계

withProperties는 각 프로젝트에서 pivotTo로 작업 링크를 따라가고, 필요한 상태를 where로 거른 뒤 aggregate("$count")를 수행합니다. 종류별로 전체, 완료, 진행 중, 시작 전을 계산하므로 총 8개의 파생 속성이 있습니다.

코딩 작업의 파생 속성학습 작업의 파생 속성합산한 반환 속성
numberOfCodingTasksnumberOfLearningTasksnumberOfTasks
numberOfCompletedCodingTasksnumberOfCompletedLearningTasksnumberOfCompletedTasks
numberOfInProgressCodingTasksnumberOfInProgressLearningTasksnumberOfInProgressTasks
numberOfNotStartedCodingTasksnumberOfNotStartedLearningTasksnumberOfNotStartedTasks

서버에서 돌려받은 종류별 개수를 Number(...)로 변환하여 더합니다. 합산을 온톨로지나 데이터 처리 단계에서 미리 수행하는 설계도 가능하지만, 이 구현은 각 파생 속성을 하나의 집계로 유지하고 합계를 클라이언트에서 계산합니다.

훅 구현

dataServices/useProjects.ts에 둡니다. IProject는 Osdk.Instance<AdvanceTodoProject, never, PropertyKeys<AdvanceTodoProject>>에 합산 통계 네 개를 추가한 타입입니다.

/*
 * Copyright 2025 Palantir Technologies, Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { AdvanceTodoProject } from "@advanced-to-do-application/sdk";
import { useCallback } from "react";
import useSWR from "swr";
import { useOsdkClient } from "@osdk/react";
import type { Osdk, PropertyKeys } from "@osdk/client";

export type IProject = Osdk.Instance<AdvanceTodoProject, never, PropertyKeys<AdvanceTodoProject>> & {
  numberOfTasks: number,
  numberOfCompletedTasks: number,
  numberOfInProgressTasks: number,
  numberOfNotStartedTasks: number,
}

function useProjects() {
  const client = useOsdkClient();
  const fetcher = useCallback(async () => {
    // Count each concrete task type through its project link.
    const projectsPage = await client(AdvanceTodoProject)
      .withProperties({
        "numberOfCodingTasks": (baseObjectSet) =>
          baseObjectSet.pivotTo("codingTasks").aggregate("$count"),
        "numberOfCompletedCodingTasks": (baseObjectSet) =>
          baseObjectSet.pivotTo("codingTasks").where({
            "status": { $eq: "COMPLETED" },
          }).aggregate("$count"),
        "numberOfInProgressCodingTasks": (baseObjectSet) =>
          baseObjectSet.pivotTo("codingTasks").where({
            "status": { $eq: "IN PROGRESS" },
          }).aggregate("$count"),
        "numberOfNotStartedCodingTasks": (baseObjectSet) =>
          baseObjectSet.pivotTo("codingTasks").where({
            "status": { $eq: "NOT STARTED" },
          }).aggregate("$count"),
        "numberOfLearningTasks": (baseObjectSet) =>
          baseObjectSet.pivotTo("learningTasks").aggregate("$count"),
        "numberOfCompletedLearningTasks": (baseObjectSet) =>
          baseObjectSet.pivotTo("learningTasks").where({
            "status": { $eq: "COMPLETED" },
          }).aggregate("$count"),
        "numberOfInProgressLearningTasks": (baseObjectSet) =>
          baseObjectSet.pivotTo("learningTasks").where({
            "status": { $eq: "IN PROGRESS" },
          }).aggregate("$count"),
        "numberOfNotStartedLearningTasks": (baseObjectSet) =>
          baseObjectSet.pivotTo("learningTasks").where({
            "status": { $eq: "NOT STARTED" },
          }).aggregate("$count"),
      })
      .fetchPage();

    const projects: IProject[] = projectsPage.data.map((project) => {
      return {
        ...project,
        numberOfTasks: Number(project.numberOfCodingTasks) + Number(project.numberOfLearningTasks),
        numberOfCompletedTasks: Number(project.numberOfCompletedCodingTasks) + Number(project.numberOfCompletedLearningTasks),
        numberOfInProgressTasks: Number(project.numberOfInProgressCodingTasks) + Number(project.numberOfInProgressLearningTasks),
        numberOfNotStartedTasks: Number(project.numberOfNotStartedCodingTasks) + Number(project.numberOfNotStartedLearningTasks),
      };
    });
    return projects;
  }, [client]);

  const { data, isLoading, isValidating, error } = useSWR<IProject[]>(
    "projects",
    fetcher,
    { revalidateOnFocus: false }
  );

  return {
    projects: data ?? [],
    isLoading,
    isValidating,
    isError: error,
  };
}

export default useProjects;

SWR 키는 projects이며, 같은 캐시 범위에서 이를 공유합니다. revalidateOnFocus: false는 창에 다시 포커스가 생겼을 때의 자동 재검증을 끕니다. 폴링이나 객체 구독은 설정하지 않습니다.

반환값의미
projects프로젝트 배열. 데이터가 없을 때는 빈 배열입니다.
isLoading첫 데이터를 기다리는 로딩 상태입니다.
isValidating초기 조회나 재검증 요청이 진행 중인지 나타냅니다.
isError조회에 실패한 오류값이며 성공 여부를 나타내는 Boolean이 아닙니다.

projects가 비어 있다는 사실만으로 조회 성공을 판단하지 않습니다. 컴포넌트는 로딩과 오류 상태를 함께 확인해야 합니다. 실패는 SWR로 전달하며, 별도의 재시도 설정이 없으므로 SWR의 기본 오류 재시도 정책을 따릅니다.

화면에서 사용

프로젝트 이름과 네 통계를 표시합니다. CSS 클래스는 스타일을 붙일 지점이며 데이터 조회와 통계 계산에는 영향을 주지 않습니다.

import React from 'react';
import useProjects, { type IProject } from '../dataServices/useProjects';

const ProjectsPage: React.FC = () => {
  const { projects, isLoading, isError } = useProjects();
  
  if (isLoading) return <div>Loading projects...</div>;
  if (isError) return <div>Error loading projects: {isError.message}</div>;
  
  return (
    <div className="projects-container">
      <h1>Projects ({projects.length})</h1>
      
      {projects.length === 0 ? (
        <div className="empty-state">No projects found</div>
      ) : (
        <div className="projects-grid">
          {projects.map(project => (
            <ProjectCard 
              key={project.$primaryKey} 
              project={project}
            />
          ))}
        </div>
      )}
    </div>
  );
};

// ProjectCard component using the data
const ProjectCard: React.FC<{ project: IProject }> = ({ project }) => {
    
  return (
    <div className="project-card">
      <h3>{project.name}</h3>
      <div className="task-stats">
        <div>Total Tasks: {project.numberOfTasks}</div>
        <div>Completed: {project.numberOfCompletedTasks}</div>
        <div>In Progress: {project.numberOfInProgressTasks}</div>
        <div>Not Started: {project.numberOfNotStartedTasks}</div>
      </div>
    </div>
  );
};

export default ProjectsPage;

갱신과 조회 범위

컴포넌트가 훅을 호출하면 SWR은 캐시된 프로젝트를 제공하고, 조회가 필요한 경우 fetcher를 실행합니다. 서버의 종류별 집계 결과를 합산한 배열이 캐시에 저장되면 화면이 다시 렌더링됩니다. 캐시가 있어도 SWR의 재검증 조건에 따라 요청할 수 있습니다.

이 구현은 fetchPage()를 한 번 호출하고 nextPageToken을 따라가지 않습니다. 화면의 프로젝트 수는 로드한 페이지의 수이며 온톨로지 전체 프로젝트 수를 뜻하지 않습니다. 프로젝트가 많으면 페이지 이동을 구성해야 합니다.

통계는 조회 또는 재검증한 시점의 값입니다. 작업 목록의 구독이 바뀌어도 이 projects 키를 자동으로 무효화하지는 않습니다. 두 화면의 통계를 함께 갱신하려면 프로젝트 키도 재검증해야 합니다.