Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
131 commits
Select commit Hold shift + click to select a range
23dbe48
feat: 스토리북 초기 설정
Hys-Lee May 21, 2025
698ed91
chore: 로컬 환경 설정 gitignore에 추가
Hys-Lee May 21, 2025
b6cb35d
feat: 디자인 토큰 tailwind 적용 설정
Hys-Lee May 22, 2025
55f4a75
Merge branch 'main' of upstream
Hys-Lee May 22, 2025
2763cf3
Merge branch 'main' into design
Hys-Lee May 22, 2025
cf2157a
feat: 스토리북 배포용 공용 컴포넌트 설정
Hys-Lee May 22, 2025
4ec8eaf
feat: tailwind설정에 postcss관련 설정 임시 복원 및 storybook 배포 설정
Hys-Lee May 22, 2025
2b3894e
feat: 스토리북 배포 관련 설정 추가
Hys-Lee May 22, 2025
d1ca615
feat: 디자인 시스템 스토리북 초기 및 테스트용 설정
Hys-Lee May 22, 2025
0a0ea18
feat: 프리티어 및 린트 설치 및 설정 (임시)
Hys-Lee May 22, 2025
eda3e43
style: 린트 및 프리티어로 인한 코드 스타일 변경
Hys-Lee May 22, 2025
86acdfe
feat: 디자인 시스템 스토리북 배포 설정 수정
Hys-Lee May 22, 2025
978b3aa
feat: token 변환 설정파일 수정
Hys-Lee May 24, 2025
a16fd87
docs: 디자인 시스템 관련 리드미 템플릿 수정
Hys-Lee May 24, 2025
ee6afc7
feat: 디자인 시스템 패키지 배포 테스트 성공
Hys-Lee May 24, 2025
70ebaa3
chore: tailwind/postcss 버전 복구
Hys-Lee May 24, 2025
bcc1cd5
feat: 스토리북 스타일 autodocs처리
Hys-Lee May 24, 2025
7d5250b
docs: 디자인 시스템 관련 문서들 정리(임시)
Hys-Lee May 24, 2025
02b17e6
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee May 24, 2025
7041351
pull main
Hys-Lee May 24, 2025
5bf678b
feat: 토큰 설정 및 스토리북 배포 설정 수정
Hys-Lee May 24, 2025
5104f2a
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee May 25, 2025
df656bb
Merge branch 'main' into design
Hys-Lee May 25, 2025
a5446c8
feat: 빌드 관련 tailwindcss/postcss 버전 수정 및 gitignore 포함
Hys-Lee May 25, 2025
fabde4e
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee May 25, 2025
68ce4ea
feat: ci linux환경에서의 스토리북 빌드 관련 패키지 설정 수정
Hys-Lee May 26, 2025
21316d4
Merge branch 'design' into main
Hys-Lee May 26, 2025
272432d
Merge branch 'design'
Hys-Lee May 26, 2025
2e05c03
feat: 디자인 시스템 스토리북 예시 수정
Hys-Lee May 26, 2025
583c613
feat: zustand설치
Hys-Lee May 26, 2025
d29eeff
feat: 상현 개인 레포에서 스토리북 배포 동작하도록 수정
Hys-Lee May 26, 2025
7a7a68a
feat: deploy-storybook path에 자기자신 포함해, 변경 시 동작하도록 수정
Hys-Lee May 26, 2025
3136c68
fix: deploy-storybook path경로 수정
Hys-Lee May 26, 2025
8801660
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee Jun 5, 2025
5c7a59d
feat: Chromatic UI 테스팅 github action 실험 적용
Hys-Lee Jun 8, 2025
3dc406c
feat: chromatic 설치
Hys-Lee Jun 8, 2025
b2629d4
feat: chromatic ci 관련 yarn->npm으로 변경
Hys-Lee Jun 8, 2025
dfac342
feat: chromatic 실험용 Test 컴폰너트 ui 변경
Hys-Lee Jun 8, 2025
f065d94
feat: chromatic github action 패키지 버전 수정
Hys-Lee Jun 8, 2025
525b888
feat: chromatic ci 중단 옵션 수정
Hys-Lee Jun 8, 2025
8bf61b6
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee Jun 8, 2025
f08aa2a
Merge branch 'main' into design
Hys-Lee Jun 8, 2025
08b4560
feat: upstream PR에 대해 작동하도록 수정
Hys-Lee Jun 8, 2025
ee7a20e
feat: 크로마틱 연동 테스트
Hys-Lee Jun 10, 2025
1eb042b
feat: 스토리북 plop통한 템플릿 생성 기능 추가
Hys-Lee Jun 10, 2025
396aab4
docs: 스토리북 Intro 문서 수정
Hys-Lee Jun 10, 2025
9387475
feat: 크로마틱 연동 upstream에 적용하도록 수정
Hys-Lee Jun 10, 2025
ab5fa12
docs: 스토리북 Intro 문구 수정
Hys-Lee Jun 10, 2025
8ee477b
docs: 스토리북 intro 스타일 다듬기
Hys-Lee Jun 10, 2025
d294635
docs: 스토리북 버전 및 change log관련 문구 수정
Hys-Lee Jun 10, 2025
55a220a
feat: 스토리북 배포 주소 추가
Hys-Lee Jun 10, 2025
f07a016
docs: 스토리북 intro 문서 스타일 수정.
Hys-Lee Jun 10, 2025
e1b6af9
docs: 프로젝트 README에 디자인시스템 배포 페이지 추가
Hys-Lee Jun 10, 2025
891c5e5
docs: 스토리북 Intro 스타일 수정
Hys-Lee Jun 10, 2025
c2f7bec
docs: 프로젝트 README에 디자인시스템 배포 페이지 추가
Hys-Lee Jun 10, 2025
08de64e
Merge branch 'design'
Hys-Lee Jun 11, 2025
3f3ad2d
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee Jun 11, 2025
3d64154
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee Jun 12, 2025
99ac5b6
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee Jun 13, 2025
1589563
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee Jun 13, 2025
6a2f808
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee Jun 13, 2025
25a404b
Trigger CI/CD for redeploy
Hys-Lee Jun 15, 2025
23bc5da
Trigger CI/CD for storybook redeploy
Hys-Lee Jun 15, 2025
af4bcf6
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee Jun 15, 2025
7c97603
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee Jun 15, 2025
d494c02
feat: 스토리북 배포 ci 관련 캐싱 삭제 및 cleanup 동작 추가
Hys-Lee Jun 15, 2025
d313a9c
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee Jun 18, 2025
059e897
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee Jun 25, 2025
15c7ec4
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee Jun 26, 2025
2e7f446
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee Jun 26, 2025
b2f3216
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee Jun 26, 2025
5f81d91
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee Jun 28, 2025
c4a0e97
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee Jun 28, 2025
b7c9b3a
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee Jun 28, 2025
074ebf4
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee Jun 28, 2025
bc22043
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee Jul 5, 2025
010a449
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee Jul 9, 2025
a5e0ba9
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee Jul 14, 2025
be8d56d
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee Jul 23, 2025
4b3ae63
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee Jul 24, 2025
3ac8db2
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee Jul 25, 2025
3544b0a
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee Jul 25, 2025
3df8115
Update README.md
Hys-Lee Jul 29, 2025
f290846
docs: 리드미 목록 관련 수정
Hys-Lee Jul 30, 2025
6d9f881
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee Aug 11, 2025
e8f1572
fix: 스토리북 배포 favicon 경로 수정
Hys-Lee Aug 11, 2025
5e429ea
feat: 모든 fetching에 대해 디바운스 처리 및 fetch 에러에 대해 toast동작 추가
Hys-Lee Aug 12, 2025
96c1443
fix: 바텀시트로 인한 바텀태그 클릭 불능 방지
Hys-Lee Aug 12, 2025
81f8a50
feat: 바텀시트 전역 관리 제작
Hys-Lee Aug 12, 2025
2880789
feat: TodoBottomSheet 전역관리 맞게 수정 및 DetailBody에 적용
Hys-Lee Aug 12, 2025
2b186c7
feat: BottomSheetRenderer 히스토리 구조 적용 및 바텀시트 변경에 대해 리마운트 및 모바일 키보드 동작
Hys-Lee Aug 15, 2025
0fa30b6
feat: 바텀시트 관련 훅 구조 히스토리로 변경
Hys-Lee Aug 15, 2025
48d36d9
feat: 바텀시트 훅 구조 변경에 따른 TodoBottomSheet 적용부분인 DetailBody, GoalCard수정
Hys-Lee Aug 15, 2025
7a294f4
feat: 바텀시트 훅 변경에 따른 TodoResultBottomSheet 수정 및 사용부 수정
Hys-Lee Aug 15, 2025
f996952
feat: 바텀시트 훅 관련 ListCard 수정에서 타입 처리
Hys-Lee Aug 15, 2025
fc1fc8c
feat: 바텀시트 훅으로 인한 GoalDurationBottomSheet 관련 변경
Hys-Lee Aug 15, 2025
0843420
feat: 메인 페이지 수정
Hys-Lee Aug 16, 2025
e551646
feat: 상세 페이지 ui 수정 및 ModalAddingTodo추가
Hys-Lee Aug 16, 2025
8084437
feat: GuestGroup 제작
Hys-Lee Aug 16, 2025
4c51f90
feat: GuestGroup생성으로 인한 GroupChatItem 및 GroupChatRoom변경
Hys-Lee Aug 16, 2025
7335ef3
feat: 게스트 모드 진입과 탈출에서 상태 처리
Hys-Lee Aug 17, 2025
398f6c5
Merge branch 'fix-demo' into update-after-demo
Hys-Lee Aug 17, 2025
d4527a1
feat: 바텀시트 훅 추가에서 타입 에러 수정
Hys-Lee Aug 17, 2025
80b7d35
feat: 게스트 모드 위한 모킹 구조 제작
Hys-Lee Aug 17, 2025
43e0d7b
feat: 게스트 모드 적용
Hys-Lee Aug 17, 2025
e5eebd3
feat: 빌드 에러 수정
Hys-Lee Aug 17, 2025
742c292
feat: 린트 에러 수정
Hys-Lee Aug 17, 2025
fc3f8b0
fix: DoneItemDetail 스토리북 관련 빌드 에러 수정
Hys-Lee Aug 17, 2025
6b0e02b
feat: 스토리북(크로마틱) 빌드 에러 수정
Hys-Lee Aug 17, 2025
b05e3a5
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee Aug 17, 2025
efee88a
Merge branch 'main' into update-after-demo
Hys-Lee Aug 17, 2025
71b6498
feat: 게스트모드 관련 마이페이시 처리
Hys-Lee Aug 17, 2025
b78646d
feat: 목표 달성 관련 추가사항 반영
Hys-Lee Aug 17, 2025
8543b55
feat: 상세페이지 TodoBottomSheet 제거
Hys-Lee Aug 18, 2025
e750173
fix: 목표 추가 버그 수정
Hys-Lee Aug 18, 2025
7cd8527
fix: 목표 생성 후 todo가 없어도 목표 완성 모달 뜨는 버그 수정
Hys-Lee Aug 18, 2025
78d9b87
fix: 상세 페이지 남을 날짜 NaN(undefined경우)는 표기 안하도록 수정
Hys-Lee Aug 18, 2025
4d28b75
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee Aug 22, 2025
075108f
feat: api debouncer 최적화
Hys-Lee Aug 22, 2025
92fce12
feat: MSW 종료 처리 추가
Hys-Lee Aug 22, 2025
96cb9c9
fix: 체크박스 공통 컴포넌트 제어방식에 checked css 적용 씹히지 않도록 수정
Hys-Lee Aug 24, 2025
5aa099c
feat: 투두 체크에 대해 낙관적 업데이트 적용
Hys-Lee Aug 24, 2025
c07c9be
feat: 낙관적 업데이트 적용에 따른 UI 변화 애니메이션 처리
Hys-Lee Aug 24, 2025
c6b066d
feat: 낙관적 업데이트 대비 isValidating 사용
Hys-Lee Aug 24, 2025
e3d324e
feat: ListCard 더보기 조건 수정
Hys-Lee Aug 24, 2025
53f6b09
feat: 엑세스 토큰 만료에 대해 재발급 처리
Hys-Lee Aug 27, 2025
48e5a1f
fix: 불필요 콘솔 제거
Hys-Lee Aug 27, 2025
f4aee9e
Merge branch 'main' of https://github.com/prography/10th-Motimo-FE
Hys-Lee Aug 27, 2025
4766a1f
Merge branch 'main' into update-after-demo
Hys-Lee Aug 27, 2025
4a699eb
fix: 게스트모드 세부목표 관련 에러 수정
Hys-Lee Aug 27, 2025
560ac3a
feat: 게스트모드 추가 처리
Hys-Lee Aug 27, 2025
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
47 changes: 42 additions & 5 deletions api/service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Api, HttpClient } from "./generated/motimo/Api";
import { Api, HttpClient, HttpResponse } from "./generated/motimo/Api";
import useAuthStore from "../stores/useAuthStore";
import useToastStore from "@/stores/useToastStore";

Expand All @@ -18,10 +18,12 @@ const httpClient = new HttpClient({
}

const isGuest = useAuthStore.getState().isGuest;
if (isGuest) {
// 게스트거나 토큰 만료의 경우
if (isGuest || !token) {
return { format: "json" };
}

// 임의의 경우를 위해 남겨둠.
return {};
},
});
Expand All @@ -33,19 +35,23 @@ const showToast = (content: string, createdAt: Date) => {

// Debouncer 감싸도 될 것 같은데?
const debounceer = <T, E>(apiRequest: typeof httpClient.request<T, E>) => {
const timeLimit = 1000;
let timer: number;
const timeLimit = 300;
const timerDictionary: { [apiFullUrl: string]: number } = {};
let rejectTimer: (reason?: any) => void;
return (
requestParams: Parameters<typeof httpClient.request<T, E>>[0],
): ReturnType<typeof httpClient.request<T>> => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix return type generic

Missing E in ReturnType<typeof httpClient.request<T, E>> causes typing drift.

The diff above adjusts the signature at this line.

🤖 Prompt for AI Agents
In api/service.ts around line 43, the function return type uses
ReturnType<typeof httpClient.request<T>> but is missing the error generic E,
causing typing drift; update the function signature to accept the error generic
(add <E = unknown> where appropriate) and change the return type to
ReturnType<typeof httpClient.request<T, E>> so the httpClient.request call's
error type is propagated; ensure any related type params and callers are updated
to include or default E to avoid breaking changes.

const apiFullUrl = `${requestParams.path}?${requestParams.query}`;
const timer = timerDictionary[apiFullUrl];

Comment on lines +44 to 46
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Serialize requestParams.query properly

"${requestParams.query}" will become [object Object] and collide across distinct queries.

The diff above already replaces lines 44-46 with a safe URLSearchParams serialization.

🤖 Prompt for AI Agents
In api/service.ts around lines 44 to 46, requestParams.query is being
string-interpolated which yields "[object Object]" and causes collisions;
replace that interpolation by serializing the query properly (e.g., build a
URLSearchParams from requestParams.query and use its toString()) so apiFullUrl
includes the encoded query string, and then use that serialized string to look
up timerDictionary.

if (timer) {
clearTimeout(timer);
rejectTimer("debouncing");
}
Comment on lines 37 to 50
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Debouncer has race/incorrect rejection issues; make timers and rejectors per-URL and reject with a cancel sentinel

  • Single rejectTimer shared across URLs will reject the wrong promise under concurrency.
  • On clearing, rejecting with a bare string may surface as an error in consumers.
  • Also, timerDictionary should use ReturnType<typeof setTimeout>.

Apply:

-const debounceer = <T, E>(apiRequest: typeof httpClient.request<T, E>) => {
-  const timeLimit = 300;
-  const timerDictionary: { [apiFullUrl: string]: number } = {};
-  let rejectTimer: (reason?: any) => void;
+const debounceer = <T, E>(apiRequest: typeof httpClient.request<T, E>) => {
+  const timeLimit = 300;
+  const timerDictionary: Record<string, ReturnType<typeof setTimeout>> = {};
+  const rejectDictionary: Record<string, (reason?: any) => void> = {};
   return (
-    requestParams: Parameters<typeof httpClient.request<T, E>>[0],
-  ): ReturnType<typeof httpClient.request<T>> => {
-    const apiFullUrl = `${requestParams.path}?${requestParams.query}`;
-    const timer = timerDictionary[apiFullUrl];
+    requestParams: Parameters<typeof httpClient.request<T, E>>[0],
+  ): ReturnType<typeof httpClient.request<T, E>> => {
+    const query =
+      typeof requestParams.query === "string"
+        ? requestParams.query
+        : requestParams.query
+        ? new URLSearchParams(requestParams.query as Record<string, any>).toString()
+        : "";
+    const apiFullUrl = `${requestParams.path}${query ? `?${query}` : ""}`;
+    const timer = timerDictionary[apiFullUrl];
 
     if (timer) {
       clearTimeout(timer);
-      rejectTimer("debouncing");
+      rejectDictionary[apiFullUrl]?.(
+        Object.assign(new Error("request debounced"), {
+          name: "AbortError",
+          code: "ERR_CANCELED",
+          canceled: true,
+        })
+      );
     }
-    const apiRes: Promise<T> = new Promise((resolve, reject) => {
-      rejectTimer = reject;
-      timerDictionary[apiFullUrl] = Number(
-        // timer = Number(
-        setTimeout(async () => {
-          try {
-            const res = apiRequest(requestParams);
-            resolve(res);
-          } catch (error) {
-            console.error(error);
-            showToast(`API ERROR`, new Date());
-          }
-        }, timeLimit),
-      );
+    const apiRes: Promise<T> = new Promise((resolve, reject) => {
+      rejectDictionary[apiFullUrl] = reject;
+      timerDictionary[apiFullUrl] = setTimeout(async () => {
+        try {
+          // see token/401 handling below
+          const res = await apiRequest(requestParams);
+          resolve(res);
+        } catch (error) {
+          // rejection handled below (401 retry) or propagated
+          reject(error);
+        } finally {
+          delete timerDictionary[apiFullUrl];
+          delete rejectDictionary[apiFullUrl];
+        }
+      }, timeLimit);
     });

Also applies to: 53-65

🤖 Prompt for AI Agents
In api/service.ts around lines 37-50 (and similarly adjust lines 53-65), the
debouncer wrongly shares a single rejectTimer across all URLs and uses number
type for timers and a bare string rejection; change timerDictionary to map
apiFullUrl to ReturnType<typeof setTimeout>, and create a per-URL entry for both
timer and its rejector (e.g., store an object { timer, reject } keyed by
apiFullUrl). When clearing a timer only clear and call the corresponding per-URL
reject function, and reject with a cancel sentinel (use a dedicated Error or
Symbol, e.g., new Error('debounced') or CANCELED symbol) instead of a raw string
so consumers can identify cancellations. Ensure you remove the per-URL entries
after resolution/rejection to avoid leaks.

const apiRes: Promise<T> = new Promise((resolve, reject) => {
rejectTimer = reject;
timer = Number(
timerDictionary[apiFullUrl] = Number(
// timer = Number(
setTimeout(async () => {
try {
const res = apiRequest(requestParams);
Expand All @@ -57,10 +63,41 @@ const debounceer = <T, E>(apiRequest: typeof httpClient.request<T, E>) => {
}, timeLimit),
);
});

// 토큰 재발급 처리
tokenHandler(apiRes);

Comment on lines +67 to 69
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

401 handling doesn’t retry the original request; tokenHandler is not chained, so callers still see a rejection

Current approach calls tokenHandler(apiRes) without awaiting/rewrapping, and tokenHandler doesn’t retry. Move the 401 interception into the debounced execution and retry once after refreshing tokens.

Apply:

-    // 토큰 재발급 처리
-    tokenHandler(apiRes);
-
     return apiRes;
   };
 };
-// 토큰 처리
-const tokenHandler = async <T, E>(
-  apiRes: ReturnType<typeof httpClient.request<T, E>>,
-) => {
-  return apiRes.catch(async (e) => {
-    if (e.status === 401) {
-      const token = useAuthStore.getState().refreshToken;
-
-      if (!token) {
-        api.authController.logout();
-        throw new Error("no refresh token");
-      }
-      const tokenRes = await api.authController.reissue({
-        refreshToken: token,
-      });
-
-      if (!tokenRes?.accessToken || !tokenRes?.refreshToken) {
-        throw new Error("token reissue error");
-      }
-
-      useAuthStore.setState((states) => ({
-        ...states,
-        accessToken: tokenRes.accessToken,
-        refreshToken: tokenRes.refreshToken,
-      }));
-    }
-  });
-};
+// Refresh tokens helper
+const refreshTokens = async () => {
+  const token = useAuthStore.getState().refreshToken;
+  if (!token) {
+    api.authController.logout();
+    throw new Error("no refresh token");
+  }
+  const tokenRes = await api.authController.reissue({ refreshToken: token });
+  if (!tokenRes?.accessToken || !tokenRes?.refreshToken) {
+    throw new Error("token reissue error");
+  }
+  useAuthStore.setState((s) => ({
+    ...s,
+    accessToken: tokenRes.accessToken,
+    refreshToken: tokenRes.refreshToken,
+  }));
+};

And augment the debounced execution to retry on 401 (within the earlier diff block):

-        try {
-          // see token/401 handling below
-          const res = await apiRequest(requestParams);
-          resolve(res);
-        } catch (error) {
-          // rejection handled below (401 retry) or propagated
-          reject(error);
-        } finally {
+        try {
+          try {
+            const res = await apiRequest(requestParams);
+            resolve(res);
+          } catch (error: any) {
+            if ((error as any)?.status === 401) {
+              await refreshTokens();
+              const retried = await apiRequest(requestParams);
+              resolve(retried);
+            } else {
+              throw error;
+            }
+          }
+        } catch (finalErr) {
+          showToast(`API ERROR`, new Date());
+          reject(finalErr);
+        } finally {

Also applies to: 74-100, 55-63

return apiRes;
};
};
// 토큰 처리
const tokenHandler = async <T, E>(
apiRes: ReturnType<typeof httpClient.request<T, E>>,
) => {
return apiRes.catch(async (e) => {
if (e.status === 401) {
const token = useAuthStore.getState().refreshToken;

if (!token) {
api.authController.logout();
throw new Error("no refresh token");
}
const tokenRes = await api.authController.reissue({
refreshToken: token,
});

if (!tokenRes?.accessToken || !tokenRes?.refreshToken) {
throw new Error("token reissue error");
}

useAuthStore.setState((states) => ({
...states,
accessToken: tokenRes.accessToken,
refreshToken: tokenRes.refreshToken,
}));
}
});
};
httpClient.request = debounceer(httpClient.request);

// API 클라이언트 인스턴스 생성
Expand Down
10 changes: 8 additions & 2 deletions api/useApiQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ApiMethodReturnType,
ApiMethodParams,
} from "./service";
import useAuthStore from "@/stores/useAuthStore";

// SWR Key 타입
type SWRKey = readonly unknown[];
Expand All @@ -27,7 +28,11 @@ export function useApiQuery<

return useSWR<ApiMethodReturnType<TApiGroup, TMethod>>(
// params가 null이면 요청하지 않음 (조건부 fetching)
params === null ? null : key,
params === null
? null
: useAuthStore.getState().isGuest
? `${key}, guest`
: key,
params === null
? null
: () => {
Expand All @@ -51,7 +56,8 @@ export function useApiQuery<
? methodFunction(...params)
: methodFunction();
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorMessage =
error instanceof Error ? error.message : String(error);
throw new Error(
`Failed to call ${String(apiGroup)}.${String(method)}: ${errorMessage}`,
);
Expand Down
1 change: 0 additions & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
import ModalRenderer from "./_components/ModalRenderer";
import { MSWComponent } from "@/components/_mocks/MSWComponent";
import ToastRenderer from "./_components/ToastRenderer";
import BottomSheetRenderer from "./_components/BottomSheetRenderer";
import GuestModeHandler from "./_components/GuestModeHandler";
Expand Down
12 changes: 10 additions & 2 deletions app/onboarding/_components/LoginScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from "@/lib/constants";
import MotimoLogoBlack from "@/components/shared/public/MOTIMO_LOGO_BLACK.svg";
import useAuthStore from "@/stores/useAuthStore";
import { DB_NAME } from "@/mocks/guestMode/db";

interface LoginScreenProps {
onNext: () => void;
Expand All @@ -29,6 +30,7 @@ export default function LoginScreen({ onNext }: LoginScreenProps) {
clearOauthData,
setIsGuest,
reset,
setHasCompletedOnboarding,
} = useAuthStore();

// OAuth 콜백 처리 (URL 파라미터 방식)
Expand Down Expand Up @@ -171,12 +173,18 @@ export default function LoginScreen({ onNext }: LoginScreenProps) {
window.location.href = `${OAUTH_ENDPOINTS.KAKAO_AUTHORIZE}?redirect_uri=${redirect_uri}&state=${state}`;
};

const handleBrowse = () => {
const handleBrowse = async () => {
// TODO: Handle browse without login
reset();

// 게스트모드 로그인 기록 확인
const dbs = await indexedDB.databases();
const hasGuestDB = dbs.find((db) => db.name === DB_NAME);
if (hasGuestDB) setHasCompletedOnboarding(true);

login();
setIsGuest(true);
onNext();
// onNext();
};

return (
Expand Down
5 changes: 3 additions & 2 deletions app/onboarding/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ export default function OnboardingPage() {
useEffect(() => {
if (!hasHydrated) return;

// 이미 온보딩을 완료했으면 redirect (게스트가 아닌 경우만)
if (hasCompletedOnboarding && isLoggedIn && !isGuest) {
// 이미 온보딩을 완료했으면 redirect (게스트도 포함)
if (hasCompletedOnboarding && isLoggedIn) {
// if (hasCompletedOnboarding && isLoggedIn && !isGuest) {
router.replace("/");
return;
}
Expand Down
6 changes: 4 additions & 2 deletions components/_mocks/MSWComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { useEffect, useRef } from "react";

export const MSWComponent = () => {
const hasInitialized = useRef(false);

useEffect(() => {
if (hasInitialized.current) return;

const init = async () => {
const { initMsw } = await import("../../mocks/index");
await initMsw();
Expand All @@ -21,3 +21,5 @@ export const MSWComponent = () => {

return null;
};

// export default MSWComponent;
89 changes: 61 additions & 28 deletions components/details/ListCard/ListCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { TodoResultRqEmotionEnum } from "@/api/generated/motimo/Api";
import { subGoalApi, todoApi } from "@/api/service";
import { postTodoResult } from "@/lib/fetching/postTodoResult";
import {
makeSubgoalInfiniteOptimisticData,
useObservingExist,
// useObservingInfiniteOffset,
useSubGoalTodosAllInfinite,
Expand All @@ -27,6 +28,7 @@ import { date2StringWithSpliter } from "@/utils/date2String";

import useBottomSheet from "@/hooks/useBottomSheet";
import useGoalWithSubGoalTodo from "@/hooks/queries/useGoalWithSubGoalTodo";
import { AnimatePresence, motion } from "motion/react";
interface ListCardProps {
subGoalInfo: {
name?: string;
Expand Down Expand Up @@ -54,14 +56,14 @@ const ListCard = ({
// 이 안에서도 subGoal에대한 todod들 가져오는 fetch있어야 함.

const observingRef = useRef<HTMLDivElement | null>(null);

const {
data: fetchedTodoItemsInfo,
mutate,
isLoading,
isReachedLast,
size,
setSize,
isValidating,
} = useSubGoalTodosAllInfinite(subGoalInfo.id ?? "");

const existObserver = useObservingExist(
Expand Down Expand Up @@ -152,7 +154,9 @@ const ListCard = ({

return (
<>
<main className="w-full flex-1 p-4 inline-flex flex-col justify-start items-center gap-5 overflow-hidden">
<main
className={`w-full flex-1 p-4 inline-flex flex-col justify-start items-center gap-5 overflow-hidden ${isLoading ? "opacity-50" : ""}`}
>
{/* <section className="flex items-center gap-2 w-full justify-between">
<button
onClick={() => onLeft()}
Expand Down Expand Up @@ -289,32 +293,61 @@ const ListCard = ({
</div>
</section>

<section className="flex-1 w-full h-full gap-2 flex flex-col">
{(!checkedMore ? todoItemsInfo.slice(0, 5) : todoItemsInfo).map(
(todoInfo) => {
return (
<TodoItem
onChecked={async () => {
onTodoCheck && onTodoCheck(todoInfo.id);
// const res = await todoApi.toggleTodoCompletion(todoInfo.id);
// if (res) mutate();
}}
title={todoInfo.title}
checked={todoInfo.checked}
key={todoInfo.id}
onReportedClick={async () => {
// 일단 모달을 띄워야 함.
// postTodoResult(todoInfo.id);
setOpenBottomSheet(true);
setTodoIdForResult(todoInfo.id);
}}
reported={todoInfo.reported}
targetDate={todoInfo.targetDate}
/>
);
},
)}
{!checkedMore && todoItemsInfo.length > 0 && (
<section
className={`flex-1 w-full h-full gap-2 flex flex-col transition-opacity duration-300 ${isValidating ? "opacity-50" : ""}`}
>
<AnimatePresence>
{(!checkedMore ? todoItemsInfo.slice(0, 5) : todoItemsInfo).map(
(todoInfo) => {
const optimisticDataCallback =
makeSubgoalInfiniteOptimisticData(todoInfo.id);

return (
<motion.div
key={todoInfo.id}
layout
transition={{ duration: 0.3 }}
initial={{ opacity: 0, scale: 0.8 }} // 나타날 때 시작 상태
animate={{ opacity: 1, scale: 1 }} // 나타날 때 최종 상태
exit={{ opacity: 0, scale: 0.8 }} // 사라질 때 최종 상태
>
<TodoItem
onChecked={async () => {
await mutate(
optimisticDataCallback,
// undefined,

{
// optimisticData: optimisticDataCallback,
populateCache: false,
revalidate: false,
rollbackOnError: true,
},
);

onTodoCheck && onTodoCheck(todoInfo.id);

// const res = await todoApi.toggleTodoCompletion(todoInfo.id);
// if (res) mutate();
}}
title={todoInfo.title}
checked={todoInfo.checked}
key={todoInfo.id}
onReportedClick={async () => {
// 일단 모달을 띄워야 함.
// postTodoResult(todoInfo.id);
setOpenBottomSheet(true);
setTodoIdForResult(todoInfo.id);
}}
reported={todoInfo.reported}
targetDate={todoInfo.targetDate}
/>
</motion.div>
);
},
)}
</AnimatePresence>
Comment on lines +300 to +349
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Optimistic update misconfigured: cache isn’t updated; request isn’t awaited.

  • Passing a MutatorCallback with populateCache: false prevents the optimistic value from entering the cache, so no UI update occurs.
  • The server toggle (onTodoCheck) isn’t awaited; failures won’t roll back.

Fix by using SWR’s optimisticData + in-mutate request (auto rollback on rejection) and let SWR revalidate.

Apply:

-                    <TodoItem
-                      onChecked={async () => {
-                        await mutate(
-                          optimisticDataCallback,
-                          {
-                            // optimisticData: optimisticDataCallback,
-                            populateCache: false,
-                            revalidate: false,
-                            rollbackOnError: true,
-                          },
-                        );
-
-                        onTodoCheck && onTodoCheck(todoInfo.id);
-                      }}
+                    <TodoItem
+                      onChecked={async () => {
+                        await mutate(
+                          async (current) => {
+                            // perform server toggle; throw to trigger SWR rollback
+                            await onTodoCheck?.(todoInfo.id);
+                            return current; // keep optimistic cache until revalidate finishes
+                          },
+                          {
+                            optimisticData: optimisticDataCallback,
+                            populateCache: true,
+                            revalidate: true,
+                            rollbackOnError: true,
+                          },
+                        );
+                      }}
                       title={todoInfo.title}
                       checked={todoInfo.checked}
-                      key={todoInfo.id}
+                      /* key on motion.div is sufficient */

Optional animation polish:

-          <AnimatePresence>
+          <AnimatePresence initial={false}>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{(!checkedMore ? todoItemsInfo.slice(0, 5) : todoItemsInfo).map(
(todoInfo) => {
const optimisticDataCallback =
makeSubgoalInfiniteOptimisticData(todoInfo.id);
return (
<motion.div
key={todoInfo.id}
layout
transition={{ duration: 0.3 }}
initial={{ opacity: 0, scale: 0.8 }} // 나타날 때 시작 상태
animate={{ opacity: 1, scale: 1 }} // 나타날 때 최종 상태
exit={{ opacity: 0, scale: 0.8 }} // 사라질 때 최종 상태
>
<TodoItem
onChecked={async () => {
await mutate(
optimisticDataCallback,
// undefined,
{
// optimisticData: optimisticDataCallback,
populateCache: false,
revalidate: false,
rollbackOnError: true,
},
);
onTodoCheck && onTodoCheck(todoInfo.id);
// const res = await todoApi.toggleTodoCompletion(todoInfo.id);
// if (res) mutate();
}}
title={todoInfo.title}
checked={todoInfo.checked}
key={todoInfo.id}
onReportedClick={async () => {
// 일단 모달을 띄워야 함.
// postTodoResult(todoInfo.id);
setOpenBottomSheet(true);
setTodoIdForResult(todoInfo.id);
}}
reported={todoInfo.reported}
targetDate={todoInfo.targetDate}
/>
</motion.div>
);
},
)}
</AnimatePresence>
<AnimatePresence initial={false}>
{(!checkedMore ? todoItemsInfo.slice(0, 5) : todoItemsInfo).map(
(todoInfo) => {
const optimisticDataCallback =
makeSubgoalInfiniteOptimisticData(todoInfo.id);
return (
<motion.div
key={todoInfo.id}
layout
transition={{ duration: 0.3 }}
initial={{ opacity: 0, scale: 0.8 }} // 나타날 때 시작 상태
animate={{ opacity: 1, scale: 1 }} // 나타날 때 최종 상태
exit={{ opacity: 0, scale: 0.8 }} // 사라질 때 최종 상태
>
<TodoItem
onChecked={async () => {
await mutate(
async (current) => {
// perform server toggle; throw to trigger SWR rollback
await onTodoCheck?.(todoInfo.id);
return current; // keep optimistic cache until revalidate finishes
},
{
optimisticData: optimisticDataCallback,
populateCache: true,
revalidate: true,
rollbackOnError: true,
},
);
}}
title={todoInfo.title}
checked={todoInfo.checked}
onReportedClick={async () => {
// 일단 모달을 띄워야 함.
setOpenBottomSheet(true);
setTodoIdForResult(todoInfo.id);
}}
reported={todoInfo.reported}
targetDate={todoInfo.targetDate}
/>
</motion.div>
);
},
)}
</AnimatePresence>
🤖 Prompt for AI Agents
components/details/ListCard/ListCard.tsx lines 300-349: the mutate call
currently passes a MutatorCallback with populateCache:false and revalidate:false
so the optimistic value never enters the cache and the server toggle
(onTodoCheck) isn’t awaited so failures won’t rollback; change to use SWR’s
optimisticData pattern by calling mutate with an optimisticData object
representing the toggled todo and an in-mutate async function that performs
await onTodoCheck(todoInfo.id) (or the API call) and returns the new server
data, keep rollbackOnError:true, and allow revalidation (remove
populateCache:false and revalidate:false) so the UI updates immediately, errors
roll back, and SWR revalidates afterward.

{!checkedMore && todoItemsInfo.length > 5 && (
<button
type="button"
className="w-full h-8 px-2 py-1 flex justify-center items-center"
Expand Down
17 changes: 11 additions & 6 deletions components/shared/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,20 @@ const Checkbox = ({ ...props }: CheckboxProps) => {
<input
className={`
shrink-0
Comment on lines 10 to 11
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add a visible keyboard focus style

Currently focus can be invisible (appearance-none and outline-0 when checked). Ensure accessible focus indication.

Add focus-visible styles in the base block:

-          shrink-0
+          shrink-0
+          focus-visible:outline-2 focus-visible:outline-Color-primary-70
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
className={`
shrink-0
className={`
shrink-0
focus-visible:outline-2 focus-visible:outline-Color-primary-70
`}
🤖 Prompt for AI Agents
In components/shared/Checkbox/Checkbox.tsx around lines 10 to 11, the checkbox
currently uses classes that remove visible focus (e.g., appearance-none and
outline-0), so keyboard focus can be invisible; add focus-visible styles to the
base class block to ensure an accessible focus indicator (for example add
focus-visible:outline-none? No — instead add a visible focus ring/outline like
focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-primary or
focus-visible:outline focus-visible:outline-2 focus-visible:outline-primary) and
ensure these styles are not suppressed when the input is checked; update the
className to include those focus-visible utility classes so keyboard users see a
clear focus state.

${`appearance-none w-4 h-4 relative bg-background-alternative rounded outline-[1.50px] outline-offset-[-1.50px] outline-Color-gray-20 overflow-hidden
${`appearance-none w-4 h-4 relative rounded overflow-hidden
hover:outline-Color-gray-40
checked:bg-center
checked:bg-background-strong
checked:outline-0
checked:bg-[url("data:image/svg+xml,%3Csvg%20width%3D%2214%22%20height%3D%2214%22%20viewBox%3D%220%200%2014%2014%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M3.58325%207.27735L6.08343%209.77734L11.0833%204.77734%22%20stroke%3D%22white%22%20stroke-width%3D%221.5%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E")]
checked:hover:bg-Color-primary-70


${props.className ?? ""}`}
${
props.checked
? `bg-center
bg-background-strong
bg-[url("data:image/svg+xml,%3Csvg%20width%3D%2214%22%20height%3D%2214%22%20viewBox%3D%220%200%2014%2014%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M3.58325%207.27735L6.08343%209.77734L11.0833%204.77734%22%20stroke%3D%22white%22%20stroke-width%3D%221.5%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E")]
outline-0
hover:bg-Color-primary-70`
: `bg-background-alternative outline-[1.50px] outline-offset-[-1.50px] outline-Color-gray-20`
}
Comment on lines +18 to +25
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Critical: Visual state tied to props.checked breaks uncontrolled checkboxes

Relying on props.checked for styling desynchronizes UI when the input is used uncontrolled (e.g., with defaultChecked). Use Tailwind’s checked: variant so the visual state follows the actual input state.

Apply this diff to remove the runtime conditional and style via pseudo-class:

-               ${
-                 props.checked
-                   ? `bg-center
-         bg-background-strong 
-         bg-[url("data:image/svg+xml,%3Csvg%20width%3D%2214%22%20height%3D%2214%22%20viewBox%3D%220%200%2014%2014%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M3.58325%207.27735L6.08343%209.77734L11.0833%204.77734%22%20stroke%3D%22white%22%20stroke-width%3D%221.5%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E")]
-         outline-0
-          hover:bg-Color-primary-70`
-                   : `bg-background-alternative outline-[1.50px] outline-offset-[-1.50px]  outline-Color-gray-20`
-               }
+          bg-background-alternative
+          checked:bg-center
+          checked:bg-background-strong
+          checked:bg-[url("data:image/svg+xml,%3Csvg%20width%3D%2214%22%20height%3D%2214%22%20viewBox%3D%220%200%2014%2014%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M3.58325%207.27735L6.08343%209.77734L11.0833%204.77734%22%20stroke%3D%22white%22%20stroke-width%3D%221.5%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E")]
+          checked:outline-0
+          checked:hover:bg-Color-primary-70

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In components/shared/Checkbox/Checkbox.tsx around lines 18-25, the component
currently toggles visual classes based on props.checked which breaks
uncontrolled usage (defaultChecked); remove the runtime conditional and instead
apply Tailwind pseudo-class variants so the UI follows the native input state —
replace the conditional class string with static classes that use checked: or
peer-checked: variants (e.g., add "peer" to the input and use
"peer-checked:bg-center peer-checked:bg-[url(...)]
peer-checked:bg-background-strong peer-checked:outline-0
hover:peer-checked:bg-Color-primary-70" and fallback outline classes for the
unchecked state), ensuring the actual <input> remains the stateful element so
uncontrolled/defaultChecked cases render correctly.

`}
type="checkbox"
{...props}
Expand Down
Loading