Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions .agents/skills/api/timo-api-integration/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,14 @@

### Phase 3 — `_queries/` 훅 작성

- 위치: `app/(domain)/_queries/use-xxx.ts`
- 위치: `app/(domain)/_queries/use-xxx-query.ts` (조회) / `use-xxx-mutation.ts` (변경)
- 네이밍: 파일명은 `-query.ts`/`-mutation.ts`로, export하는 함수명은 `~Query`/`~Mutation`으로 끝낸다. 파일당 훅 1개가 기본이다. → `docs/conventions/naming.md` 참조
- `useSuspenseQuery`를 쓸지 `useQuery`를 쓸지는 컴포넌트가 `AsyncBoundary`(Suspense) 안에 있는지로 결정한다. `AsyncBoundary`로 감싸져 있으면 `useSuspenseQuery`를 쓴다.
- **주의**: 생성된 `getGetXxxQueryOptions()` 팩토리를 그대로 `useSuspenseQuery`에 스프레드하면 `UseQueryOptions`가 허용하는 `skipToken`과 `UseSuspenseQueryOptions`가 타입 충돌을 일으킨다. `queryKey`/`queryFn`은 생성된 `getGetXxxQueryKey` + 원본 fetcher 함수로 직접 조립한다.
- `select`에서 `BaseResponse.data`를 언랩하고 로컬 zod 스키마로 `.parse()`해서 반환한다.

조회 훅 예시 (`_queries/use-home-view-query.ts`):

```ts
"use client";

Expand All @@ -54,14 +57,36 @@ import {
} from "@/api/generated/endpoints/home/home";
import { homeViewDataSchema } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_types/home-view-type";

export const useHomeView = ({ filter, baseDate }: GetHomeViewParams) =>
export const useHomeViewQuery = ({ filter, baseDate }: GetHomeViewParams) =>
useSuspenseQuery({
queryKey: getGetHomeQueryKey({ filter, baseDate }),
queryFn: ({ signal }) => getHome({ filter, baseDate }, undefined, signal),
select: ({ data }) => homeViewDataSchema.parse(data),
});
```

변경 훅 예시 (`_queries/use-withdraw-mutation.ts`):

```ts
"use client";

import { useMutation } from "@tanstack/react-query";

import { withdraw } from "@/api/generated/endpoints/auth/auth";
import { useClearSession } from "@/app/[locale]/(main)/settings/_hooks/use-clear-session";

export const useWithdrawMutation = () => {
const clearSession = useClearSession();

return useMutation({
mutationFn: () => withdraw(),
onSuccess: () => {
clearSession();
},
});
};
```

### Phase 4 — 컨테이너 연결

- mock 함수 import를 제거하고 새 훅으로 교체한다.
Expand All @@ -70,6 +95,7 @@ export const useHomeView = ({ filter, baseDate }: GetHomeViewParams) =>

### Phase 5 — 자가 검토

- [ ] 훅 파일명/함수명이 `-query`/`~Query`, `-mutation`/`~Mutation` 컨벤션을 따르는가
- [ ] `api/generated/` 내부 파일을 직접 수정하지 않았는가 (재생성 시 사라짐)
- [ ] 응답을 로컬 zod 스키마로 `.parse()`해서 검증했는가 (gen 응답 스키마를 그대로 UI까지 노출하지 않았는가)
- [ ] `useSuspenseQuery` 사용 시 `queryKey`/`queryFn`을 직접 조립해 타입 충돌을 피했는가
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
} from "@/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-today-scroll";
import { useHomeTodosByDate } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-todos-by-date";
import { useHomeViewMode } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-view-mode";
import { useHomeView } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_queries/use-home-view";
import { useHomeViewQuery } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_queries/use-home-view-query";
import { AnimatedToast } from "@/components/toast/AnimatedToast";
import { StopCompleteModalContainer } from "@/containers/timer/StopCompleteModalContainer";
import { DetailTodoModalContainer } from "@/containers/todo-modal/detail/DetailTodoModalContainer";
Expand All @@ -39,7 +39,7 @@ export const HomeTodoContainer = () => {
const filter: HomeViewFilter = isWeekView ? "WEEK" : "DEFAULT";
const baseDate = formatDateKey(referenceDate);

const { data: homeViewData } = useHomeView({ filter, baseDate });
const { data: homeViewData } = useHomeViewQuery({ filter, baseDate });
const days = homeViewData.days;

const [pendingCompleteTodo, setPendingCompleteTodo] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import {
useReorderTodo,
} from "@/api/generated/endpoints/todo/todo";
import { reorderTodos } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_utils/todo-order";
import { useActiveTimer } from "@/hooks/use-active-timer";
import { useTimerQueryInvalidation } from "@/hooks/use-timer-query-invalidation";
import { useActiveTimer } from "@/hooks/timer/use-active-timer";
import { useTimerQueryInvalidation } from "@/hooks/timer/use-timer-query-invalidation";
import { useTimeSidebarStore } from "@/stores/time-sidebar/useTimeSidebarStore";

export interface UseHomeTodosByDateOptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from "@/api/generated/endpoints/home/home";
import { homeViewDataSchema } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_types/home-view-type";

export const useHomeView = ({ filter, baseDate }: GetHomeViewParams) =>
export const useHomeViewQuery = ({ filter, baseDate }: GetHomeViewParams) =>
useSuspenseQuery({
queryKey: getGetHomeQueryKey({ filter, baseDate }),
queryFn: ({ signal }) => getHome({ filter, baseDate }, undefined, signal),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { z } from "zod";

import { dayOfWeekSchema, todoSchema } from "@/api/common/todo-schema";
import { dayOfWeekSchema, todoSchema } from "@/schemas/todo/todo-schema";

export const homeViewFilterSchema = z.enum(["DEFAULT", "WEEK"]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export {
todoSubtaskSchema,
todoSchema,
todoCreateResponseSchema,
} from "@/api/common/todo-schema";
} from "@/schemas/todo/todo-schema";

export type {
TodoPriorityTypes,
Expand All @@ -14,4 +14,4 @@ export type {
TodoSubtask,
Todo,
TodoCreateResponseData,
} from "@/api/common/todo-schema";
} from "@/schemas/todo/todo-schema";
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

import { useEffect, useState } from "react";

import type { TodoTimerStatusTypes } from "@/api/common/todo-schema";
import type { TodoTimerStatusTypes } from "@/schemas/todo/todo-schema";
import type { ReactNode } from "react";

import {
TodayTodoCard,
type SubTodo,
type TodayTodoCardToolbar,
} from "@/app/[locale]/(main)/today/_components/TodayTodoCard";
} from "@/app/[locale]/(main)/(with-time-sidebar)/today/_components/TodayTodoCard";

export interface TodayTodoCardContainerProps {
title: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { TodoIconValue } from "@repo/timo-design-system/ui";
import { TodayDateHeaderContainer } from "@/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayDateHeaderContainer";
import { TodayTodoCardContainer } from "@/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoCardContainer";
import { useTodayTodoList } from "@/app/[locale]/(main)/(with-time-sidebar)/today/_hooks/useTodayTodoList";
import { useToday } from "@/app/[locale]/(main)/(with-time-sidebar)/today/_queries/use-today";
import { useTodayQuery } from "@/app/[locale]/(main)/(with-time-sidebar)/today/_queries/use-today-query";
import { AnimatedToast } from "@/components/toast/AnimatedToast";
import { StopCompleteModalContainer } from "@/containers/timer/StopCompleteModalContainer";
import { DetailTodoModalContainer } from "@/containers/todo-modal/detail/DetailTodoModalContainer";
Expand All @@ -30,7 +30,7 @@ const renderTodoIcon = (icon: string | undefined) => {

export const TodayTodoListContainer = () => {
const tToast = useTranslations("Toast");
const { data } = useToday();
const { data } = useTodayQuery();

const [pendingCompleteTodoId, setPendingCompleteTodoId] = useState<
number | null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import { useQueryClient } from "@tanstack/react-query";
import { useState } from "react";

import type { CreateTodoRequest } from "@/api/common/todo-schema";
import type { TodoCreateRequest } from "@/api/generated/models";
import type { CreateTodoRequest } from "@/schemas/todo/todo-schema";

import { getGetTodayQueryKey } from "@/api/generated/endpoints/home/home";
import { useCreateTodo } from "@/api/generated/endpoints/todo/todo";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import {
useChangeSubtaskStatus,
useChangeTodoStatus,
} from "@/api/generated/endpoints/todo/todo";
import { useActiveTimer } from "@/hooks/use-active-timer";
import { useTimerQueryInvalidation } from "@/hooks/use-timer-query-invalidation";
import { useActiveTimer } from "@/hooks/timer/use-active-timer";
import { useTimerQueryInvalidation } from "@/hooks/timer/use-timer-query-invalidation";
import { useTimeSidebarStore } from "@/stores/time-sidebar/useTimeSidebarStore";

export interface UseTodayTodoListOptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from "@/api/generated/endpoints/home/home";
import { todayDataSchema } from "@/app/[locale]/(main)/(with-time-sidebar)/today/_types/today-type";

export const useToday = () =>
export const useTodayQuery = () =>
useSuspenseQuery({
queryKey: getGetTodayQueryKey(),
queryFn: ({ signal }) => getToday(undefined, signal),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { z } from "zod";

import { dayOfWeekSchema, todoSchema } from "@/api/common/todo-schema";
import { dayOfWeekSchema, todoSchema } from "@/schemas/todo/todo-schema";

export const todayTodoSchema = todoSchema.extend({
date: z.string(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ import {
useChangeSubtaskStatus,
useChangeTodoStatus,
} from "@/api/generated/endpoints/todo/todo";
import { useFocusTodo } from "@/app/[locale]/(main)/focus/_queries/use-focus-todo";
import { useFocusTodoQuery } from "@/app/[locale]/(main)/focus/_queries/use-focus-todo-query";
import { convertDateToBadgeText } from "@/app/[locale]/(main)/focus/_utils/date";
import { useActiveTimer } from "@/hooks/use-active-timer";
import { useTimerActions } from "@/hooks/use-timer-actions";
import { useTimerOvertime } from "@/hooks/use-timer-overtime";
import { useTimerQueryInvalidation } from "@/hooks/use-timer-query-invalidation";
import { useActiveTimer } from "@/hooks/timer/use-active-timer";
import { useTimerActions } from "@/hooks/timer/use-timer-actions";
import { useTimerOvertime } from "@/hooks/timer/use-timer-overtime";
import { useTimerQueryInvalidation } from "@/hooks/timer/use-timer-query-invalidation";
import { convertDurationToMinutes } from "@/utils/duration/convert-duration-to-minutes";

export interface UseFocusSessionOptions {
Expand All @@ -38,7 +38,7 @@ export const useFocusSession = ({
const timerSessionControlsRef = useRef<TimerSessionControlsHandle>(null);
const wasTimeUpRef = useRef(false);

const { data: focusView } = useFocusTodo();
const { data: focusView } = useFocusTodoQuery();
const { data: activeTimer } = useActiveTimer();

const { invalidateActiveTimer, invalidateHomeView, invalidateTimeBoxes } =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from "@/api/generated/endpoints/focus/focus";
import { focusViewSchema } from "@/app/[locale]/(main)/focus/_types/task-type";

export const useFocusTodo = () =>
export const useFocusTodoQuery = () =>
useSuspenseQuery({
queryKey: getGetFocusTodoQueryKey(),
queryFn: ({ signal }) => getFocusTodo(undefined, signal),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

import { useTranslations } from "next-intl";

import type { TermsType } from "@/types/terms-type";
import type { TermsType } from "@/schemas/settings/terms-schema";

import { PolicyDocument } from "@/components/policy/PolicyDocument";
import { useTerms } from "@/queries/use-terms";
import { useTermsQuery } from "@/queries/settings/use-terms-query";

export interface SettingsTermsContainerProps {
type: TermsType;
Expand All @@ -15,7 +15,7 @@ export const SettingsTermsContainer = ({
type,
}: SettingsTermsContainerProps) => {
const t = useTranslations("Policy");
const { data: term } = useTerms(type);
const { data: term } = useTermsQuery(type);

return (
<div className="px-15 pt-7.5">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import type { SettingsWithdrawalLabels } from "@/app/[locale]/(main)/settings/_t

import { SettingsWithdrawalView } from "@/app/[locale]/(main)/settings/_components/withdrawal/SettingsWithdrawalView";
import { SettingsWithdrawModalContainer } from "@/app/[locale]/(main)/settings/_containers/withdrawal/SettingsWithdrawModalContainer";
import { useWithdrawAction } from "@/app/[locale]/(main)/settings/_queries/withdrawal/use-withdraw";
import { useWithdrawMutation } from "@/app/[locale]/(main)/settings/_queries/withdrawal/use-withdraw-mutation";
import { AnimatedToast } from "@/components/toast/AnimatedToast";

export const SettingsWithdrawalContainer = () => {
const t = useTranslations("Settings.withdrawal");
const { mutate: withdrawMutate, isPending } = useWithdrawAction();
const { mutate: withdrawMutate, isPending } = useWithdrawMutation();
const [isErrorToastOpen, setIsErrorToastOpen] = useState(false);

const labels: SettingsWithdrawalLabels = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@ import { useTranslations } from "next-intl";

import type { SettingsLanguage } from "@/app/[locale]/(main)/settings/_types/account/profile-type";

import { tagCreateDataSchema } from "@/api/common/tag-schema";
import {
getGetMyProfileQueryKey,
useUpdateLanguage,
} from "@/api/generated/endpoints/user/user";
import { UpdateLanguageRequestLanguage } from "@/api/generated/models";
import { useSettingsLanguageParam } from "@/app/[locale]/(main)/settings/_hooks/account/use-settings-language-param";
import { useLogoutAction } from "@/app/[locale]/(main)/settings/_queries/account/use-logout";
import { useCreateTag } from "@/queries/tag/use-create-tag";
import { useDeleteTag } from "@/queries/tag/use-delete-tag";
import { useTags } from "@/queries/tag/use-tags";
import { useMyProfile } from "@/queries/use-my-profile";
import { useLogoutMutation } from "@/app/[locale]/(main)/settings/_queries/account/use-logout-mutation";
import { useMyProfileQuery } from "@/queries/auth/use-my-profile-query";
import { useCreateTagMutation } from "@/queries/tag/use-create-tag-mutation";
import { useDeleteTagMutation } from "@/queries/tag/use-delete-tag-mutation";
import { useTagsQuery } from "@/queries/tag/use-tags-query";
import { tagCreateDataSchema } from "@/schemas/tag/tag-schema";
import { getDefaultTagLabelKey } from "@/utils/todo/tag-label";

const LANGUAGE_REQUEST_MAP: Record<
Expand Down Expand Up @@ -44,15 +44,15 @@ export const useSettingsProfile = () => {
const { locale, commitLanguage } = useSettingsLanguageParam();
const tCommon = useTranslations("Common");

const { data: profile } = useMyProfile();
const { data: profile } = useMyProfileQuery();

const { mutateAsync: updateLanguage } = useUpdateLanguage();
const queryClient = useQueryClient();

const tagsQuery = useTags();
const { mutate: createTag } = useCreateTag();
const { mutate: deleteTag } = useDeleteTag();
const { mutate: logoutMutate, isPending: isLoggingOut } = useLogoutAction();
const tagsQuery = useTagsQuery();
const { mutate: createTag } = useCreateTagMutation();
const { mutate: deleteTag } = useDeleteTagMutation();
const { mutate: logoutMutate, isPending: isLoggingOut } = useLogoutMutation();

const tagItems = (tagsQuery.data?.tags ?? []).map((tag) => {
const labelKey = tag.isDefault
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useMutation } from "@tanstack/react-query";
import { logout } from "@/api/generated/endpoints/auth/auth";
import { useClearSession } from "@/app/[locale]/(main)/settings/_hooks/use-clear-session";

export const useLogoutAction = () => {
export const useLogoutMutation = () => {
const clearSession = useClearSession();

return useMutation({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useMutation } from "@tanstack/react-query";
import { withdraw } from "@/api/generated/endpoints/auth/auth";
import { useClearSession } from "@/app/[locale]/(main)/settings/_hooks/use-clear-session";

export const useWithdrawAction = () => {
export const useWithdrawMutation = () => {
const clearSession = useClearSession();

return useMutation({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { useTranslations } from "next-intl";

import { StatisticsCalendar } from "@/app/[locale]/(main)/statistics/_components/StatisticsCalendar";
import { useStatisticsCalendarQuery } from "@/app/[locale]/(main)/statistics/_queries/statistics-queries";
import { useStatisticsCalendarQuery } from "@/app/[locale]/(main)/statistics/_queries/use-statistics-query";
import { LottiePlayer } from "@/components/lottie/LottiePlayer";
import { formatDateKey } from "@/utils/date/date";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { StatisticsHeaderContainer } from "@/app/[locale]/(main)/statistics/_con
import {
useStatisticsDailyQuery,
useStatisticsSummaryQuery,
} from "@/app/[locale]/(main)/statistics/_queries/statistics-queries";
} from "@/app/[locale]/(main)/statistics/_queries/use-statistics-query";
import { formatStatisticsSidePanelDate } from "@/app/[locale]/(main)/statistics/_utils/format-statistics-date";
import { formatDateKey } from "@/utils/date/date";

Expand Down
2 changes: 1 addition & 1 deletion apps/timo-web/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { Metadata } from "next";
import { routing } from "@/i18n/routing";
import { AuthProvider } from "@/providers/auth/AuthProvider";
import { LanguageSyncProvider } from "@/providers/locale/LanguageSyncProvider";
import { OverlayProvider } from "@/providers/OverlayProvider";
import { OverlayProvider } from "@/providers/overlay/OverlayProvider";
import { QueryProvider } from "@/providers/query/QueryProvider";

const pretendard = localFont({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@

import { useTranslations } from "next-intl";

import type { TermsType } from "@/types/terms-type";
import type { TermsType } from "@/schemas/settings/terms-schema";

import { PolicyDocument } from "@/components/policy/PolicyDocument";
import { useTerms } from "@/queries/use-terms";
import { useTermsQuery } from "@/queries/settings/use-terms-query";

export interface PolicyContainerProps {
type: TermsType;
}

export const PolicyContainer = ({ type }: PolicyContainerProps) => {
const t = useTranslations("Policy");
const { data: term } = useTerms(type);
const { data: term } = useTermsQuery(type);

if (!term) {
return <p className="typo-body-m-12 text-timo-gray-700">{t("empty")}</p>;
Expand Down
2 changes: 1 addition & 1 deletion apps/timo-web/app/[locale]/policy/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { AsyncBoundary } from "@/components/boundary/AsyncBoundary";
import { ROUTES } from "@/constants/routes";
import { Link } from "@/i18n/navigation";
import { routing } from "@/i18n/routing";
import { termsTypeSchema } from "@/types/terms-type";
import { termsTypeSchema } from "@/schemas/settings/terms-schema";

interface PolicyPageProps {
params: Promise<{ locale: string }>;
Expand Down
Loading
Loading