Skip to content

Commit cd25457

Browse files
authored
Main page 목표 추가 제외 완성 (#31) - toast등 비동기 반응성도 제외 (#35)
* feat: 메인 페이지 제작중 관련 svg설정 next turbopack에 적용 * feat: 메인 페이지 제작에 필요한 컴포넌트 추가 제작 GoalMenuContainer, GoalTItleArea. TodoList는 todoItem이 빈 배열이라 checkedLen필요 없을 때를 고려해 수정함. * feat: 일부 공유 컴포넌트 반응형으로 수정 고정 너비가 아니라 full로 수정함. 고정 크기일 필요가 없어보여서 임시로 수정. 추후 디자이너가 결정한 것으로 수정 가능. * feat: 메인페이지 추가 컴포넌트 AppBar 제작 * feat: msw 적용하기 * feat: 메인 페이지 제작 관련 컴포넌트 수정 svg들 폴더 위치 변경 * feat: 배너 컴포넌트 반응형으로 수정 * feat: 모달 컴포넌트 및 관련 훅과 스토어 생성 (진행중) 모달 공통 컴포넌트와 파생 컴포넌트 제작. 공통에서 종속적인 부분이 있어 합성 컴포넌트로 제작. 모달 전역 관리를 위해 zustand와 커스텀 훅을 사용. svgr관련 세팅 대해 브랜치 꼬여서 임시 저장. * feat: TodoList 접힌 상황 추가 및 api에 대한 추상화 타입 적용. 비동기 미처리. api 변동과 무관하게 사용될 타입을 생성해 적용했습니다. api의 반환 타입을 이로 변환하는 로직을 fetch함수 만들 때 적용 예정입니다. * feat: AppBar삭제 및 패키지 재설치 공유 컴포넌트에 있었어서 AppBar는 삭제함. * feat: 불필요 컴포넌트 제거(ModalAddingSubGoal겹침) * feat: 모달 버튼 공용 컴포넌트의 것으로 수정 * feat: 메인 페이지 TodoList 모달 연결 * feat: todoList관련 데이터 페칭 연결 임시 * feat: api generation 스크립트 수정 및 재생성 * feat: 날짜 타입 문자열로 변환하는 유틸 함수 제작 * feat: todo와 subGoal관련 fetching 함수 제작 이곳에 타입 변환 존재합니다 * feat: api generator fetch기반으로 변경 후 재생성 * feat: todo, todolist관련 swr 훅 제작 * feat: TodoItem 체크에 대한 낙관적 업데이트위한 훅 제작 기본 useOptimistic은 action인자가 필요하지만, 체크는 toggle이라 필요 없어서 따로 제작했습니다. 깔끔하게 만들기 위해서용. * feat: TodoList 비동기 onEdit제외 연결 onEdit은 바텀 시트 제작 후 연결. * feat: TodoList subGoal추가 관련 비동기 수정 mutate 적용 * feat: 스토리북 generation 예외 추가 완성된 도메인 및 영역은 제외시킴. * feat: shared 모노레포 구조에서 react 관련 버전 프로젝트 루트와 일치 shared에서 bottom sheet컴포넌트 제작에 외부 라이브러리 사용할 때, 버전 불일치 생겨서. * feat: check svg 수정 관련 연관 컴포넌트 TodoItem 수정 svg 색상을 외부에서 주입하도록 바꿔서, 기존에 사용하던 TodoItem에서 흰색 주입하도록 했습니다. * feat: TodoBottomSheet 완성 비동기 동작의 책임을 외부에 넘겼습니다. * feat: 데이터 페칭 이후 타입 변환의 책임을 비동기 훅으로 변경 비동기 훅은 도메인 별로 커스텀 가능한 것이기 때문에 변경했습니다. * feat: goal 관련 fetching함수 제작 * feat: TodoBottomSheet 동작 수정 초기값 바뀌는 동작 외부에서 key로 제어하지 않고, 내부에서 useEffect로 제어하도록 변경함. key로 제어하면 remount때문에 애니메이션 이상해서. * feat: todo, goal, subGoal에 대한 fethcing과 쿼리 훅 생성 데이터 처리를 쿼리 훅 내부에서 하도록 수정. mutate는 대부분 낙관적 업데이트로 사용하지 않기 때문에 변환 로직으로 덮어쓰지 않았음. * feat: TodoBottomSheet활성화, 선택된 goal관련 zustand store생성 이들 데이터는 멀리 떨어진 컴포넌트 사이에서 사용되기 때문에 전역 상태 사용함. * feat: msw모킹 위한 api 및 db 생성 * feat: 메인 페이지 goal관련 데이터 갖는 GoalCard 컴포넌트 생성 (진행중) * feat: 메인 페이지에 필요한 컴포넌트 생성 및 수정 MainHeader, GoalMenuContainer (임시) 문구 api연결은 아직 안했고, 컴포넌트 구분만 해뒀습니다. GoalMenuContainer는 선택에 대해 zustand로 전역 상태 처리하도록 수정했습니다. * feat: 메인 페이지 연결을 위한 자잘한 수정 fetchTemplate는 위치 변경으로 인한 수정. GoalMenu는 api연결에 필요한 타입 export TodoItem은 UI가 반응형으로 동작하도록 수정 * feat: TodoList 메인페이지 api연결을 위한 수정 쿼리 훅과 zustand 사용해 동작하도록 수정함. edit버튼 클릭에 대한 동작 추가함. UI반응형으로 동작하도록 w-full로 수정함. * fix: 바텀 시트 todo추가 동작 수정, 바텀시트 선택된 subgoal 버튼에 보이도록 수정 todo추가 동작 수정에 대해서, 기존 분기처리 잘못해 update를 실행하던 것을 create실행하도록 수정 * fix: TodoBottomSheet 상태 초기화 수정 initTodoInfo를 매번 object 새로 생성해 넘겨줘서 useEffect가 동작했음. 이에 컴폰너트 내부에서 분해해서 사용하도록 수정함. * feat: GoalCard 모달이 존재하면 바텀시트 닫도록 수정. 이렇지 않으면 dialog가 rule로 지정된 바텀시트에 focus-trap이 걸림. * feat: useModal 수정 바텀시트 사용할 때 모달의 사용을 알 수 있도록 isOpen을 추가함. * feat: TodoList에서 ModalAddingSubGoal 동작 관련 연결 subgoal을 추가할 수 있도록 api제작 및 연결 * fix: 자잘한 UI 에러 수정 * feat: 페칭 및 훅 자잘한 수정 use client추가 및 타입 수정 * feat: useTodoList undefined도 가능하도록 수정. 사용하는 입장에서 undefined반환 값으로 처리 분기할 수 있도록. * feat: cheer, point 관련 fetching 제작 goalFetching에서 불필요 부분 삭제 * feat: 모킹 구조 변경 및 points, cheer 핸들러 추가 관리하기 쉽도록 분리했습니다. * feat: 불필요 부분 삭제 콘솔 제거 및 element정리 * feat: fetch 동작에서 헤더 설정 수정 get요청에서도 authorization등 적용되도록 수정 * feat: 목표 추가 버튼 제작 모달 클릭 후 페이지 이동은 아직 구현하지 않았습니다. 해당 페이지 구현 마친 후 라우팅 추가하겠습니다. * feat: GoalCard와 useGoalWithSubGoalTodo대해 타입 및 props명 변경에 따른 수정 * feat: BottomTabBar Link추가로 페이지 이동할 수 있도록 함. * feat: main페이지에 BottomTabBar추가에 따른 UI수정 (이전 커밋에서 GoalCard에서 pb추가) TodoBottomSheet는 바텀탭바 존재에 따라 위치 변경 가능하도록 수정. 추가로, 여기서, 기본 subGoal 지정. * feat: main page에서 bottom tab bar관련 불필요 패딩 제거. * feat: 로컬 개발 시 msw사용하도록 수정 발표할 때 필요할 것 같아서. * feat: 시연 전 ui 수정사항 반영 * feat: 스토리북 빌드 에러 수정 컴포넌트들 props변경에 따른 에러 수정. * feat: shared 모노 레포 관련 유틸 함수 처리 shared내부에 동일한 것으로 복제했습니다. * feat: 페이지 이동 관련 수정 onboarding에서 Link를 통해 이동 fetch 시 로컬스토리지에서 access token사용하도록 변경 메인 페이지 수정 * fix: PR 전 정리 BottomTabBar 불필요 key제거 TodoList className정리, scroll 되도록 수정, 불필요 낙관적 업데이트 제거 * feat: AppBar 상단 고정 및 관련해 z-index 수정 AppBar보다 BottomSheet와 Modal이 z-index 높도록 수정 * feat: 메인페이지 bottom tab bar추가 및 bottom sheet와 함께 모바일 넓이에 맞게 조정 body자체를 scrollbar에 의해 layout shift발생하지 않게 수정하고, bottom sheet에도 적용했습니다. component/shared 패키지 버전 수정에 따른 lock파일 변경이 추가로 있습니다. bottom tabbar의 fixed로 인해 modal과의 z-index차이를 조정했습니다. 상단 AppBar관련해서 배너 상단에 공간을 만들어 ui겹침을 제거했습니다. * fix: TodoBottomSheet 버그 수정 엔터로 입력하면 높이 이상해지는 버그 수정했습니다. focus관련한 버그여서 blur처리로 수정했습니다. * fix: fetching 반환값 반환하도록 해서 이후 동작 연결되도록 수정 반환값을 보고 결과를 판단하도록 로직을 짰는데, 반환하지 않아서 제대로 작동하지 않았었습니당. * feat: dev서버 배포 전 프록시 및 msw수정 주소 동적 연결되게 수정 및 msw 주석처리. 이외 자잘한 오류 정리
1 parent 34b33e6 commit cd25457

File tree

84 files changed

+22773
-316
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

84 files changed

+22773
-316
lines changed

api/generated/motimo/Api.ts

Lines changed: 181 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,12 @@ export interface TodoResultRs {
238238
fileUrl?: string;
239239
}
240240

241+
export interface ErrorResponse {
242+
/** @format int32 */
243+
statusCode?: number;
244+
message?: string;
245+
}
246+
241247
export interface PointRs {
242248
/**
243249
* 사용자가 현재 획득한 포인트
@@ -380,18 +386,10 @@ export enum TodoResultRsEmotionEnum {
380386
GROWN = "GROWN",
381387
}
382388

383-
import type {
384-
AxiosInstance,
385-
AxiosRequestConfig,
386-
HeadersDefaults,
387-
ResponseType,
388-
} from "axios";
389-
import axios from "axios";
390-
391389
export type QueryParamsType = Record<string | number, any>;
390+
export type ResponseFormat = keyof Omit<Body, "body" | "bodyUsed">;
392391

393-
export interface FullRequestParams
394-
extends Omit<AxiosRequestConfig, "data" | "params" | "url" | "responseType"> {
392+
export interface FullRequestParams extends Omit<RequestInit, "body"> {
395393
/** set parameter to `true` for call `securityWorker` for this request */
396394
secure?: boolean;
397395
/** request path */
@@ -401,157 +399,239 @@ export interface FullRequestParams
401399
/** query params */
402400
query?: QueryParamsType;
403401
/** format of response (i.e. response.json() -> format: "json") */
404-
format?: ResponseType;
402+
format?: ResponseFormat;
405403
/** request body */
406404
body?: unknown;
405+
/** base url */
406+
baseUrl?: string;
407+
/** request cancellation token */
408+
cancelToken?: CancelToken;
407409
}
408410

409411
export type RequestParams = Omit<
410412
FullRequestParams,
411413
"body" | "method" | "query" | "path"
412414
>;
413415

414-
export interface ApiConfig<SecurityDataType = unknown>
415-
extends Omit<AxiosRequestConfig, "data" | "cancelToken"> {
416+
export interface ApiConfig<SecurityDataType = unknown> {
417+
baseUrl?: string;
418+
baseApiParams?: Omit<RequestParams, "baseUrl" | "cancelToken" | "signal">;
416419
securityWorker?: (
417420
securityData: SecurityDataType | null,
418-
) => Promise<AxiosRequestConfig | void> | AxiosRequestConfig | void;
419-
secure?: boolean;
420-
format?: ResponseType;
421+
) => Promise<RequestParams | void> | RequestParams | void;
422+
customFetch?: typeof fetch;
421423
}
422424

425+
export interface HttpResponse<D extends unknown, E extends unknown = unknown>
426+
extends Response {
427+
data: D;
428+
error: E;
429+
}
430+
431+
type CancelToken = Symbol | string | number;
432+
423433
export enum ContentType {
424434
Json = "application/json",
435+
JsonApi = "application/vnd.api+json",
425436
FormData = "multipart/form-data",
426437
UrlEncoded = "application/x-www-form-urlencoded",
427438
Text = "text/plain",
428439
}
429440

430441
export class HttpClient<SecurityDataType = unknown> {
431-
public instance: AxiosInstance;
442+
public baseUrl: string = "http://158.179.175.134:8080";
432443
private securityData: SecurityDataType | null = null;
433444
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
434-
private secure?: boolean;
435-
private format?: ResponseType;
445+
private abortControllers = new Map<CancelToken, AbortController>();
446+
private customFetch = (...fetchParams: Parameters<typeof fetch>) =>
447+
fetch(...fetchParams);
436448

437-
constructor({
438-
securityWorker,
439-
secure,
440-
format,
441-
...axiosConfig
442-
}: ApiConfig<SecurityDataType> = {}) {
443-
this.instance = axios.create({
444-
...axiosConfig,
445-
baseURL: axiosConfig.baseURL || "http://158.179.175.134:8080",
446-
});
447-
this.secure = secure;
448-
this.format = format;
449-
this.securityWorker = securityWorker;
449+
private baseApiParams: RequestParams = {
450+
credentials: "same-origin",
451+
headers: {},
452+
redirect: "follow",
453+
referrerPolicy: "no-referrer",
454+
};
455+
456+
constructor(apiConfig: ApiConfig<SecurityDataType> = {}) {
457+
Object.assign(this, apiConfig);
450458
}
451459

452460
public setSecurityData = (data: SecurityDataType | null) => {
453461
this.securityData = data;
454462
};
455463

456-
protected mergeRequestParams(
457-
params1: AxiosRequestConfig,
458-
params2?: AxiosRequestConfig,
459-
): AxiosRequestConfig {
460-
const method = params1.method || (params2 && params2.method);
464+
protected encodeQueryParam(key: string, value: any) {
465+
const encodedKey = encodeURIComponent(key);
466+
return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`;
467+
}
468+
469+
protected addQueryParam(query: QueryParamsType, key: string) {
470+
return this.encodeQueryParam(key, query[key]);
471+
}
472+
473+
protected addArrayQueryParam(query: QueryParamsType, key: string) {
474+
const value = query[key];
475+
return value.map((v: any) => this.encodeQueryParam(key, v)).join("&");
476+
}
477+
478+
protected toQueryString(rawQuery?: QueryParamsType): string {
479+
const query = rawQuery || {};
480+
const keys = Object.keys(query).filter(
481+
(key) => "undefined" !== typeof query[key],
482+
);
483+
return keys
484+
.map((key) =>
485+
Array.isArray(query[key])
486+
? this.addArrayQueryParam(query, key)
487+
: this.addQueryParam(query, key),
488+
)
489+
.join("&");
490+
}
491+
492+
protected addQueryParams(rawQuery?: QueryParamsType): string {
493+
const queryString = this.toQueryString(rawQuery);
494+
return queryString ? `?${queryString}` : "";
495+
}
496+
497+
private contentFormatters: Record<ContentType, (input: any) => any> = {
498+
[ContentType.Json]: (input: any) =>
499+
input !== null && (typeof input === "object" || typeof input === "string")
500+
? JSON.stringify(input)
501+
: input,
502+
[ContentType.JsonApi]: (input: any) =>
503+
input !== null && (typeof input === "object" || typeof input === "string")
504+
? JSON.stringify(input)
505+
: input,
506+
[ContentType.Text]: (input: any) =>
507+
input !== null && typeof input !== "string"
508+
? JSON.stringify(input)
509+
: input,
510+
[ContentType.FormData]: (input: any) =>
511+
Object.keys(input || {}).reduce((formData, key) => {
512+
const property = input[key];
513+
formData.append(
514+
key,
515+
property instanceof Blob
516+
? property
517+
: typeof property === "object" && property !== null
518+
? JSON.stringify(property)
519+
: `${property}`,
520+
);
521+
return formData;
522+
}, new FormData()),
523+
[ContentType.UrlEncoded]: (input: any) => this.toQueryString(input),
524+
};
461525

526+
protected mergeRequestParams(
527+
params1: RequestParams,
528+
params2?: RequestParams,
529+
): RequestParams {
462530
return {
463-
...this.instance.defaults,
531+
...this.baseApiParams,
464532
...params1,
465533
...(params2 || {}),
466534
headers: {
467-
...((method &&
468-
this.instance.defaults.headers[
469-
method.toLowerCase() as keyof HeadersDefaults
470-
]) ||
471-
{}),
535+
...(this.baseApiParams.headers || {}),
472536
...(params1.headers || {}),
473537
...((params2 && params2.headers) || {}),
474538
},
475539
};
476540
}
477541

478-
protected stringifyFormItem(formItem: unknown) {
479-
if (typeof formItem === "object" && formItem !== null) {
480-
return JSON.stringify(formItem);
481-
} else {
482-
return `${formItem}`;
542+
protected createAbortSignal = (
543+
cancelToken: CancelToken,
544+
): AbortSignal | undefined => {
545+
if (this.abortControllers.has(cancelToken)) {
546+
const abortController = this.abortControllers.get(cancelToken);
547+
if (abortController) {
548+
return abortController.signal;
549+
}
550+
return void 0;
483551
}
484-
}
485552

486-
protected createFormData(input: Record<string, unknown>): FormData {
487-
if (input instanceof FormData) {
488-
return input;
489-
}
490-
return Object.keys(input || {}).reduce((formData, key) => {
491-
const property = input[key];
492-
const propertyContent: any[] =
493-
property instanceof Array ? property : [property];
553+
const abortController = new AbortController();
554+
this.abortControllers.set(cancelToken, abortController);
555+
return abortController.signal;
556+
};
494557

495-
for (const formItem of propertyContent) {
496-
const isFileType = formItem instanceof Blob || formItem instanceof File;
497-
formData.append(
498-
key,
499-
isFileType ? formItem : this.stringifyFormItem(formItem),
500-
);
501-
}
558+
public abortRequest = (cancelToken: CancelToken) => {
559+
const abortController = this.abortControllers.get(cancelToken);
502560

503-
return formData;
504-
}, new FormData());
505-
}
561+
if (abortController) {
562+
abortController.abort();
563+
this.abortControllers.delete(cancelToken);
564+
}
565+
};
506566

507-
public request = async <T = any, _E = any>({
567+
public request = async <T = any, E = any>({
568+
body,
508569
secure,
509570
path,
510571
type,
511572
query,
512573
format,
513-
body,
574+
baseUrl,
575+
cancelToken,
514576
...params
515577
}: FullRequestParams): Promise<T> => {
516578
const secureParams =
517-
((typeof secure === "boolean" ? secure : this.secure) &&
579+
((typeof secure === "boolean" ? secure : this.baseApiParams.secure) &&
518580
this.securityWorker &&
519581
(await this.securityWorker(this.securityData))) ||
520582
{};
521583
const requestParams = this.mergeRequestParams(params, secureParams);
522-
const responseFormat = format || this.format || undefined;
523-
524-
if (
525-
type === ContentType.FormData &&
526-
body &&
527-
body !== null &&
528-
typeof body === "object"
529-
) {
530-
body = this.createFormData(body as Record<string, unknown>);
531-
}
532-
533-
if (
534-
type === ContentType.Text &&
535-
body &&
536-
body !== null &&
537-
typeof body !== "string"
538-
) {
539-
body = JSON.stringify(body);
540-
}
584+
const queryString = query && this.toQueryString(query);
585+
const payloadFormatter = this.contentFormatters[type || ContentType.Json];
586+
const responseFormat = format || requestParams.format;
541587

542-
return this.instance
543-
.request({
588+
return this.customFetch(
589+
`${baseUrl || this.baseUrl || ""}${path}${queryString ? `?${queryString}` : ""}`,
590+
{
544591
...requestParams,
545592
headers: {
546593
...(requestParams.headers || {}),
547-
...(type ? { "Content-Type": type } : {}),
594+
...(type && type !== ContentType.FormData
595+
? { "Content-Type": type }
596+
: {}),
548597
},
549-
params: query,
550-
responseType: responseFormat,
551-
data: body,
552-
url: path,
553-
})
554-
.then((response) => response.data);
598+
signal:
599+
(cancelToken
600+
? this.createAbortSignal(cancelToken)
601+
: requestParams.signal) || null,
602+
body:
603+
typeof body === "undefined" || body === null
604+
? null
605+
: payloadFormatter(body),
606+
},
607+
).then(async (response) => {
608+
const r = response.clone() as HttpResponse<T, E>;
609+
r.data = null as unknown as T;
610+
r.error = null as unknown as E;
611+
612+
const data = !responseFormat
613+
? r
614+
: await response[responseFormat]()
615+
.then((data) => {
616+
if (r.ok) {
617+
r.data = data;
618+
} else {
619+
r.error = data;
620+
}
621+
return r;
622+
})
623+
.catch((e) => {
624+
r.error = e;
625+
return r;
626+
});
627+
628+
if (cancelToken) {
629+
this.abortControllers.delete(cancelToken);
630+
}
631+
632+
if (!response.ok) throw data;
633+
return data.data;
634+
});
555635
};
556636
}
557637

@@ -881,14 +961,14 @@ export class Api<SecurityDataType extends unknown> {
881961
* @summary 세부 목표별 TODO 목록 조회
882962
* @request GET:/v1/sub-goals/{subGoalId}/todos/incomplete-or-date
883963
* @secure
884-
* @response `200` `CustomSlice` TODO 목록 조회 성공
885-
* @response `400` `(TodoRs)[]` 잘못된 요청 데이터
964+
* @response `200` `(TodoRs)[]` TODO 목록 조회 성공
965+
* @response `400` `ErrorResponse` 잘못된 요청 데이터
886966
*/
887967
getIncompleteOrTodayTodos: (
888968
subGoalId: string,
889969
params: RequestParams = {},
890970
) =>
891-
this.http.request<CustomSlice, TodoRs[]>({
971+
this.http.request<TodoRs[], ErrorResponse>({
892972
path: `/v1/sub-goals/${subGoalId}/todos/incomplete-or-date`,
893973
method: "GET",
894974
secure: true,

api/generator.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const generate = async (domain, url) => {
1919
generateApi({
2020
output: PATH_TO_OUTPUT_DIR,
2121
url,
22-
httpClientType: "axios", // or "fetch"
22+
httpClientType: "fetch", // or "fetch"
2323
defaultResponseAsSuccess: false,
2424
generateClient: true,
2525
generateRouteTypes: false,

0 commit comments

Comments
 (0)