AI 역문서 · 1D1S-client@ad5edf9 · 1D1S-server-v2@f69a4eb 기준 · 코드에서 거꾸로 만든 초안입니다. 근거 링크로 확인하세요.
관련 소스 파일
이 페이지는 1D1S-client의 HTTP 요청 계층과 공통 응답 추출 방식을 설명하고, @tanstack/react-query의 QueryClient와 연결되는 캐시 무효화 방식을 정리합니다. 또한 1D1S-server-v2가 사용하는 공통 성공 응답, 오프셋 페이지네이션 DTO, 예외 처리와 오류 코드의 구조를 함께 다룹니다.
이 원문에는 실제 도메인별 컨트롤러, 구체적인 API 엔드포인트, React Query의 개별 useQuery·useMutation 훅 구현이 포함되어 있지 않습니다. 따라서 해당 이름과 화면별 호출 관계는 확인 가능한 범위에서만 설명하고, 확인되지 않은 내용은 확인 필요에 남깁니다.
관련 내용은 1.3 시스템 아키텍처, 3.1 인증과 세션, 3.3 도메인 데이터 모델, 4.1 클라이언트 화면 구조를 참고합니다.
출처: client.ts:1-24, queryInvalidation.ts:1-19
클라이언트 요청 계층은 Axios 인스턴스를 생성하는 client.ts, 요청 파라미터와 응답 데이터를 처리하는 request.ts, 공통 타입을 정의하는 types.ts, React Query 캐시를 무효화하는 queryInvalidation.ts로 나뉩니다.
| 구성 요소 | 역할 | 확인되는 동작 |
|---|---|---|
apiClient | 인증이 필요한 일반 요청 | handleUnauthorized: true 옵션으로 생성합니다. |
publicApiClient | 공개 요청 | handleUnauthorized: false 옵션으로 생성합니다. |
tokenClient | 토큰·로그아웃 관련 직접 요청 | 별도의 Axios 인스턴스이며 순환 임포트 방지를 위한 용도로 선언되어 있습니다. |
silentAuthClient | 선택적 인증 요청 | 401 또는 302를 받으면 토큰을 한 번 갱신하고 원래 요청을 재시도합니다. |
requestData | 공통 성공 응답에서 data 추출 | ApiResponse<TData>를 받아 response.data.data를 반환합니다. |
requestBody | 응답 바디 전체 반환 | 응답을 래핑하지 않고 response.data를 반환합니다. |
buildQueryString | 쿼리 문자열 생성 | 배열 값은 같은 키로 여러 번 추가하고, null·undefined는 제외합니다. |
invalidateAll | 여러 React Query 키 무효화 | 전달된 키마다 queryClient.invalidateQueries를 호출합니다. |
apiClient, publicApiClient, tokenClient, silentAuthClient는 모두 API_BASE_URL과 DEFAULT_TIMEOUT_MS를 사용합니다. 기본 Axios 설정에는 withCredentials: true가 포함되어 있어 요청에 자격 증명 정보를 함께 보낼 수 있도록 구성되어 있습니다. apiClient와 publicApiClient에는 maxRedirects: 0도 설정되어 있습니다.
silentAuthClient는 요청별 _retried 플래그를 확인하므로 동일 요청의 무한 재시도를 방지합니다. 갱신에는 refreshAccessTokenOnce()를 사용하며, 갱신 실패는 내부에서 삼키고 원래 오류를 다시 거부합니다. 따라서 최종 로그아웃이나 화면 전환을 호출부가 처리하는 구조인지 여부는 해당 호출부 코드 확인이 필요합니다.
출처: client.ts:3-24, client.ts:26-68, request.ts:1-5, queryInvalidation.ts:1-19
클라이언트의 일반적인 요청은 Axios 인스턴스를 선택하고, 필요하면 requestData 또는 requestBody로 응답을 정규화하는 순서로 진행됩니다. 이 원문에서 구체적인 기능별 API 함수는 제공되지 않았으므로, 아래 흐름은 확인된 공통 계층의 동작만 나타냅니다.
apiClient, 공개 요청에는 publicApiClient를 사용합니다.tokenClient를 사용합니다.buildQueryString이 Record<string, QueryParamValue>를 URLSearchParams로 변환합니다.appendParam에 의해 동일한 키를 반복해서 추가합니다. 값이 null 또는 undefined이면 쿼리에서 제외합니다.requestData는 서버 공통 응답의 data만 반환합니다. 예를 들어 서버가 ApiResponse<TData>로 감싼 경우 호출부에는 TData가 전달됩니다.ApiResponse 형태가 아닌 경우에는 requestBody가 바디 전체를 그대로 반환합니다.401 또는 302를 받으면 silentAuthClient가 refreshAccessTokenOnce()를 실행한 뒤 같은 요청을 한 번 재시도합니다.서버 쪽에서는 ApiResponse<T>가 성공 응답을 message와 data로 표현합니다. 따라서 requestData는 서버의 data 필드와 직접 연결되는 추출 함수입니다. 반면 requestBody를 사용해야 하는 API의 범위나 해당 API가 실제로 존재하는지는 제공된 자료만으로 확인할 수 없습니다.
출처: request.ts:7-58, client.ts:35-64, ApiResponse.java:12-27
제공된 코드에서 React Query와 직접 연결되는 요소는 invalidateAll입니다. 이 함수는 QueryClient와 여러 query key를 받아 각 키에 대해 invalidateQueries를 호출합니다. 성공 콜백에서 여러 캐시를 갱신해야 할 때 반복 호출을 줄이는 공통 함수로 사용할 수 있습니다.
| 입력 | 타입 | 동작 |
|---|---|---|
queryClient | QueryClient | React Query 캐시를 관리합니다. |
keys | `ReadonlyArray<readonly unknown[] \ | null \ |
| 유효한 키 | readonly unknown[] | invalidateQueries를 호출합니다. |
null·undefined | 선택적 키 | 아무 동작도 하지 않고 건너뜁니다. |
캐시 무효화 흐름은 다음과 같습니다.
invalidateAll을 호출합니다.null 또는 undefined로 전달할 수 있습니다.invalidateAll은 유효한 키만 순회하며 queryClient.invalidateQueries({ queryKey: key })를 실행합니다.useQuery·useMutation 사용 방식은 이 원문에 구현되어 있지 않습니다.따라서 이 페이지에서 확인할 수 있는 것은 “성공 후 관련 query key를 무효화하는 공통 도우미가 있다”는 점입니다. 특정 화면의 목록과 상세 데이터가 어떤 키로 묶이는지, mutation이 어떤 API를 호출하는지, 무효화 후 즉시 재조회되는지는 개별 훅 파일을 추가로 확인해야 합니다.
서버의 ApiResponse<T>는 모든 성공 응답을 표현하는 제네릭 래퍼입니다. 필드는 message와 data이며, @JsonInclude(JsonInclude.Include.NON_NULL)에 의해 null인 필드는 JSON에 포함되지 않습니다.
| 타입 또는 필드 | 설명 |
|---|---|
ApiResponse<T> | 성공 응답의 공통 래퍼입니다. |
message | 응답 메시지입니다. 선택적 값이며 null이면 JSON에서 제외됩니다. |
data | 실제 응답 데이터입니다. 목록, 상세 DTO 등 제네릭 타입으로 표현합니다. |
ApiResponse.success(String msg) | 메시지만 포함한 성공 응답을 생성합니다. |
ApiResponse.success(String msg, T data) | 메시지와 데이터를 함께 포함한 성공 응답을 생성합니다. |
LikeDto | likedByMe, likeCnt를 제공하는 좋아요 요약 데이터입니다. |
LikeMemberResponse | 좋아요를 누른 회원의 memberId, nickname, profileImage, withdrawn을 제공합니다. |
클라이언트의 ApiResponse<TData>도 같은 구조를 정의합니다. 클라이언트 타입에서는 message?: string, data: TData로 선언되어 있어 message가 없을 수 있지만 data는 필수입니다. requestData는 이 구조를 전제로 data만 호출부에 전달합니다.
좋아요 관련 응답은 클라이언트의 LikeInfo와 서버의 LikeDto가 같은 필드 이름을 사용합니다. 두 타입 모두 likedByMe: boolean과 likeCnt: number 또는 long을 가집니다. 회원 요약 응답에서는 서버의 LikeMemberResponse.from(Member)가 getDisplayNickname(), getDisplayProfileUrl(), isWithdrawn()을 사용하므로 탈퇴 회원 표시값을 서버 DTO 생성 단계에서 결정합니다.
출처: types.ts:1-20, ApiResponse.java:3-27, LikeDto.java:9-21, LikeMemberResponse.java:8-32
원문에는 오프셋 방식과 커서 방식에 사용할 수 있는 DTO가 함께 존재합니다. 클라이언트 공통 타입인 OffsetPageInfo와 서버의 OffsetPagination<T>는 필드 구성이 대응합니다.
| 구조 | 필드 | 의미 |
|---|---|---|
OffsetPagination<T> | items | 현재 페이지의 데이터 목록입니다. |
OffsetPagination<T> | pageInfo | 오프셋 페이지 정보입니다. |
OffsetPagination.PageInfo | page | 현재 페이지 번호입니다. |
OffsetPagination.PageInfo | size | 페이지 크기입니다. |
OffsetPagination.PageInfo | totalElements | 전체 항목 수입니다. |
OffsetPagination.PageInfo | totalPages | 전체 페이지 수입니다. |
OffsetPagination.PageInfo | hasNextPage | 다음 페이지 존재 여부입니다. |
OffsetPageInfo | 동일한 다섯 메타데이터 | 클라이언트가 수신할 수 있는 공통 오프셋 페이지 정보입니다. |
Pagination<T> | items, pageInfo | PageInfo를 사용하는 일반 페이지 래퍼입니다. |
PageInfo | limit, hasNextPage, nextCursor | 커서 기반 페이지 정보로 사용할 수 있는 필드입니다. |
서버의 OffsetPagination.from(Page<T> page)는 Spring Data의 Page<T>에서 현재 페이지 정보와 항목을 변환합니다. page.getNumber(), page.getSize(), page.getTotalElements(), page.getTotalPages(), page.hasNext()가 각각 PageInfo로 복사됩니다.
반면 Pagination<T>는 items와 PageInfo를 포함하지만, 제공된 코드에는 이 객체를 생성하는 변환 메서드가 없습니다. PageInfo의 nextCursor가 실제 어느 엔드포인트에서 사용되는지, 오프셋과 커서 방식을 도메인별로 어떻게 선택하는지는 확인 필요합니다.
출처: types.ts:22-29, OffsetPagination.java:8-35, Pagination.java:9-16, PageInfo.java:8-16
서버의 GlobalExceptionHandler는 @RestControllerAdvice로 등록되어 컨트롤러 처리 과정에서 발생하는 예외를 공통 오류 응답으로 변환합니다. 도메인 예외는 CustomException, 요청 형식 오류는 Spring 예외 타입, 예상하지 못한 오류는 Exception으로 구분합니다.
| 예외 | HTTP 상태 | 오류 응답 |
|---|---|---|
CustomException | ErrorCode의 상태 | ErrorResponse.of(errorCode, retryAfterSeconds) |
MethodArgumentTypeMismatchException | 400 | 파라미터 형식 오류 |
MissingServletRequestParameterException | 400 | 필수 파라미터 누락 |
HttpMessageNotReadableException | 400 | 요청 바디 형식 오류 |
MethodArgumentNotValidException | 400 | 첫 번째 검증 메시지 |
HttpMediaTypeNotSupportedException | 415 | 지원하지 않는 Content-Type |
HttpRequestMethodNotSupportedException | 405 | 지원하지 않는 HTTP 메서드 |
DataIntegrityViolationException | 409 또는 오류 코드 상태 | 데이터 충돌 |
처리되지 않은 Exception | 500 | Internal Server Error |
CustomException에 retryAfterSeconds가 있으면 Retry-After 헤더도 추가됩니다. 데이터 무결성 오류 중 원인 메시지에 uk_member_phone_number가 포함되면 PHONE_NUMBER_ALREADY_EXISTS 오류 코드로 변환하고, 그 외에는 409와 "요청이 기존 데이터와 충돌합니다."를 반환합니다.
클라이언트는 ApiErrorResponse와 NormalizedApiError 타입을 선언합니다.
| 클라이언트 타입 | 필드 |
|---|---|
ApiErrorResponse | 선택적 message, 선택적 code |
NormalizedApiError | 선택적 status, 선택적 code, 필수 message |
다만 제공된 자료에는 서버의 ErrorResponse 클래스와 Axios 오류를 NormalizedApiError로 변환하는 함수가 없습니다. 따라서 서버가 실제 JSON에서 오류 필드를 어떤 이름으로 출력하는지, 클라이언트가 status를 HTTP 상태에서 어떻게 복사하는지는 확인 필요합니다.
출처: types.ts:6-15, GlobalExceptionHandler.java:24-179
ErrorCode는 도메인별 오류의 HTTP 상태, 코드 문자열, 사용자 메시지를 함께 정의하는 enum입니다. 예를 들어 인증 오류에는 UNAUTHORIZED와 EXPIRED_ACCESS_TOKEN이 있고, 회원 오류에는 MEMBER_NOT_FOUND와 NICKNAME_ALREADY_EXISTS가 있습니다. 챌린지·일지·채팅·댓글·알림 등도 각 도메인별 코드 범위를 가집니다.
| 코드 예시 | HTTP 상태 | 의미 |
|---|---|---|
UNAUTHORIZED | 401 | 인증되지 않은 접근입니다. |
EXPIRED_ACCESS_TOKEN | 401 | 액세스 토큰이 만료되었습니다. |
NICKNAME_ALREADY_EXISTS | 409 | 닉네임이 이미 사용 중입니다. |
DIARY_INVALID_IMAGE_URL | 400 | 허용되지 않은 이미지 URL입니다. |
CHALLENGE_NOT_FOUND | 404 | 챌린지를 찾을 수 없습니다. |
CHAT_SERVER_BUSY | 429 | 채팅 요청이 많습니다. |
CHAT_CONNECTION_LIMIT | 429 | 채팅 연결이 많습니다. |
BOOK_SEARCH_FAILED | 502 | 책 검색을 하지 못했습니다. |
클라이언트의 silentAuthClient는 HTTP 상태 401과 302를 인증 만료 신호로 보고 토큰 갱신을 시도합니다. 갱신 요청은 refreshAccessTokenOnce()를 통해 전역 single-flight 방식으로 공유한다고 코드 주석에 설명되어 있으며, 한 요청에 대해서는 _retried 플래그로 한 번만 재시도합니다.
반면 429, 409, 400 등 일반 오류를 React Query가 자동 재시도하는지, Retry-After 헤더를 어떤 훅이나 UI가 사용하는지는 제공된 코드에 없습니다. 서버가 Retry-After를 설정하는 것은 확인되지만, 클라이언트의 재시도 정책은 별도 설정 파일과 React Query 프로바이더 코드를 확인해야 합니다.
출처: client.ts:33-64, GlobalExceptionHandler.java:65-75, ErrorCode.java:7-35, ErrorCode.java:36-61, ErrorCode.java:178-200
@RestControllerAdvice인 GlobalExceptionHandler만 있고 @RestController 컨트롤러는 없습니다.apiClient, publicApiClient, silentAuthClient를 각각 호출하는 도메인별 API 함수와 화면은 무엇입니까?useQuery, useMutation, QueryClientProvider 설정과 실제 query key 규칙은 어디에 있습니까?ApiResponse<T>가 모든 REST 성공 응답에 사용됩니까? requestBody가 필요한 실제 엔드포인트는 무엇입니까?ErrorResponse 클래스와 실제 오류 JSON 필드 구조는 어떻게 정의되어 있습니까? 클라이언트의 ApiErrorResponse 및 NormalizedApiError와 어떤 변환 규칙으로 연결됩니까?OffsetPagination과 Pagination 중 각 목록 API가 사용하는 방식은 무엇입니까? PageInfo.nextCursor를 사용하는 커서 페이지네이션 엔드포인트가 존재합니까?refreshAccessTokenOnce()의 토큰 저장 방식과 갱신 실패 시 호출부의 로그아웃 처리는 어디에 구현되어 있습니까?Retry-After 헤더를 클라이언트가 읽고 재시도 또는 사용자 안내에 사용하는 코드가 있습니까?출처: request.ts:36-58, queryInvalidation.ts:10-18, GlobalExceptionHandler.java:65-75, Pagination.java:13-16