Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions .env
Copy link
Collaborator

Choose a reason for hiding this comment

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

.env 파일은 GitHub 레포지토리에 포함되지 않아야 합니다 🥲
노션이나 디스코드를 통해 파일로 따로 공유해주시는 것이 좋을 것 같아요.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VITE_API_ENDPOINT = https://bootcamp-api.codeit.kr/api/14-1/the-julge
13 changes: 13 additions & 0 deletions src/apis/client/interceptors.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { InternalAxiosRequestConfig } from "axios";

export const requestInterceptor = (
config: InternalAxiosRequestConfig,
): InternalAxiosRequestConfig => {
const token = localStorage.getItem("token") ?? "";
Copy link
Collaborator

Choose a reason for hiding this comment

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

localStorage.getItem('token')은 'token'이나 null을 반환할 것 같습니다.
그래서 아래와 같이 타입을 명시적으로 표시해주어도 좋을 것 같습니다 !

const token: string | null = localStorage.getItem('token') ?? null;


if (token) {
config.headers.Authorization = `Bearer ${token}`;
}

return config;
};
18 changes: 18 additions & 0 deletions src/apis/client/requestor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import axios, { AxiosError, AxiosInstance } from "axios";

import { requestInterceptor } from "./interceptors";

const requestor: AxiosInstance = axios.create({
baseURL: (import.meta.env.VITE_API_ENDPOINT as string).trim(),
timeout: 60000,
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
});

requestor.interceptors.request.use(requestInterceptor, (error: AxiosError) =>
Promise.reject(error),
Copy link
Collaborator

Choose a reason for hiding this comment

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

반환값이기에 return을 추가하면 좋을 것 같습니다 !

return Promise.reject(error)

);

export default requestor;
Empty file removed src/apis/index.ts
Empty file.
25 changes: 25 additions & 0 deletions src/apis/services/alertService.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { AxiosResponse } from "axios";
import { AlertListResponse, AlertResponse } from "src/types";

import requestor from "../client/requestor";

class AlertService {
Copy link
Collaborator

Choose a reason for hiding this comment

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

현재 AlertService가 class로 정의되어 있는데요.
간단한 API 호출만 처리하고 있기 때문에 getAlerts와 putAlert를 함수로 작성해도 좋을 것 같습니다 !

export const getAlerts = () => {}
export const putAlert = () => {}

getAlerts(
userId: string,
offset?: number,
Copy link
Collaborator

Choose a reason for hiding this comment

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

알림을 위에서부터 최신순으로 정렬해서 보여줄 것 같은데, 그러면 offset은 기본값으로 0으로 설정해 주면 좋을 것 같습니다 !

offset: number = 0,

limit?: number,
): Promise<AxiosResponse<AlertListResponse>> {
return requestor.get(`/users/${userId}/alerts`, {
params: { offset, limit },
});
}

putAlert(
userId: string,
alertId: string,
): Promise<AxiosResponse<AlertResponse>> {
return requestor.put(`/users/${userId}/alerts/${alertId}`);
}
}

export default new AlertService();
52 changes: 52 additions & 0 deletions src/apis/services/applicationService.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { AxiosResponse } from "axios";
import {
ApplicationListResponse,
ApplicationResponse,
ApplicationStatus,
} from "src/types";

import requestor from "../client/requestor";

class ApplicationService {
Copy link
Collaborator

Choose a reason for hiding this comment

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

ApplicationService도 AlertService와 마찬가지로 함수로 작성해도 좋을 것 같습니다 !

export const getShopApplications = () => {}
export const postApplication = () => {}
               .
               .
               .

Copy link
Collaborator

Choose a reason for hiding this comment

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

각 메서드들이 어떤 기능을 하는 메서드인지 주석을 한 줄 정도 추가해도 좋을 것 같습니다 !

// 가게의 특정 공고의 지원 목록 조회
getShopApplications() {} 

getShopApplications(
shopId: string,
noticeId: string,
offset?: number,
limit?: number,
): Promise<AxiosResponse<ApplicationListResponse>> {
return requestor.get(`/shops/${shopId}/notices/${noticeId}/applications`, {
params: { offset, limit },
});
}

postApplication(
shopId: string,
noticeId: string,
): Promise<AxiosResponse<ApplicationResponse>> {
return requestor.post(`/shops/${shopId}/notices/${noticeId}/applications`);
}

putApplication(
shopId: string,
noticeId: string,
applicationId: string,
status: Exclude<ApplicationStatus, "pending">,
): Promise<AxiosResponse<ApplicationResponse>> {
return requestor.put(
`/shops/${shopId}/notices/${noticeId}/applications/${applicationId}`,
{ status },
);
}

getUserApplications(
userId: string,
offset?: number,
limit?: number,
): Promise<AxiosResponse<ApplicationListResponse>> {
return requestor.get(`/users/${userId}/applications`, {
params: { offset, limit },
});
}
}

export default new ApplicationService();
47 changes: 47 additions & 0 deletions src/apis/services/authenticationService.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { AxiosResponse } from "axios";
import type { UserItem } from "src/types";
import { ApiWrapper } from "src/types";

import requestor from "../client/requestor";

interface LoginItem {
token: string;
user: ApiWrapper<UserItem>;
}

export type LoginRequest = { email: string; password: string };
export type LoginResponse = ApiWrapper<LoginItem>;

class AuthenticationService {
Copy link
Collaborator

Choose a reason for hiding this comment

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

AuthenticationService도 위와 마찬가지로 class 대신 함수로 작성해도 좋을 것 같습니다 !

async postAuthentication(
Copy link
Collaborator

Choose a reason for hiding this comment

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

postAuthentication는 로그인 처리 및 토큰과 사용자 정보를 로컬스토리지에 저장하는 메서드인 것 같은데,
이것도 주석으로 어떤 기능인지 간단하게 적어도 좋을 것 같습니다 !

payload: LoginRequest,
): Promise<AxiosResponse<LoginResponse>> {
const res = await requestor.post<LoginResponse>("/token", payload);

const token = res.data.item.token;
const user = res.data.item.user.item;

localStorage.setItem("token", token);
localStorage.setItem("user", JSON.stringify(user));

return res;
}

logout() {
localStorage.removeItem("token");
localStorage.removeItem("user");
}

getToken() {
return localStorage.getItem("token");
}
isAuthenticated() {
return Boolean(this.getToken());
}
getUser(): UserItem | null {
const raw = localStorage.getItem("user");
return raw ? (JSON.parse(raw) as UserItem) : null;
}
}

export default new AuthenticationService();
37 changes: 37 additions & 0 deletions src/apis/services/imageService.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import axios, { AxiosResponse } from "axios";
import { ApiWrapper } from "src/types";

import requestor from "../client/requestor";

interface PresignedItem {
url: string;
}
type PresignedResponse = ApiWrapper<PresignedItem>;

class ImageService {
async postImage(name: string): Promise<string> {
const res: AxiosResponse<PresignedResponse> = await requestor.post(
"/images",
{ name },
);
return res.data.item.url;
}

async putImage(
presignedURL: string,
file: File | Blob,
): Promise<AxiosResponse<void>> {
return axios.put(presignedURL, file, {
headers: { "Content-Type": file.type || "application/octet-stream" },
});
}

getPublicURL(presignedURL: string) {
return presignedURL.split("?")[0];
}
getImage(publicURL: string): Promise<AxiosResponse<Blob>> {
return axios.get(publicURL, { responseType: "blob" });
}
}

export default new ImageService();
57 changes: 57 additions & 0 deletions src/apis/services/noticeService.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { AxiosResponse } from "axios";
import {
NoticeListResponse,
NoticeResponse,
SortKey,
NoticePayload,
} from "src/types";

import requestor from "../client/requestor";

class NoticeService {
getNotices(params?: {
Copy link
Collaborator

Choose a reason for hiding this comment

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

getNotices 메서드는 여래개 파라미터를 관리하고 있으므로, interface로 따로 정의해서 객체로 묶어서 관리해도 좋을 것 같습니다 !

interface GetNoticesParams {
  offset?: number;
  limit?: number;
  address?: string;
  keyword?: string;
  startsAtGte?: string;
  hourlyPayGte?: number;
  sort?: SortKey;
}

getNotices(params: GetNoticesParams) ...

offset?: number;
limit?: number;
address?: string;
keyword?: string;
startsAtGte?: string;
hourlyPayGte?: number;
sort?: SortKey;
}): Promise<AxiosResponse<NoticeListResponse>> {
return requestor.get("/notices", { params });
}

getShopNotices(
shopId: string,
offset?: number,
limit?: number,
): Promise<AxiosResponse<NoticeListResponse>> {
return requestor.get(`/shops/${shopId}/notices`, {
params: { offset, limit },
});
}

postNotice(
shopId: string,
payload: NoticePayload,
): Promise<AxiosResponse<NoticeResponse>> {
return requestor.post(`/shops/${shopId}/notices`, payload);
}

getNotice(
shopId: string,
noticeId: string,
): Promise<AxiosResponse<NoticeResponse>> {
return requestor.get(`/shops/${shopId}/notices/${noticeId}`);
}

putNotice(
shopId: string,
noticeId: string,
payload: NoticePayload,
): Promise<AxiosResponse<NoticeResponse>> {
return requestor.put(`/shops/${shopId}/notices/${noticeId}`, payload);
}
}

export default new NoticeService();
23 changes: 23 additions & 0 deletions src/apis/services/shopService.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { AxiosResponse } from "axios";
import { ShopPayload, ShopResponse } from "src/types";

import requestor from "../client/requestor";

class ShopService {
postShop(payload: ShopPayload): Promise<AxiosResponse<ShopResponse>> {
return requestor.post("/shops", payload);
}

getShop(shopId: string): Promise<AxiosResponse<ShopResponse>> {
return requestor.get(`/shops/${shopId}`);
}

putShop(
shopId: string,
payload: ShopPayload,
): Promise<AxiosResponse<ShopResponse>> {
return requestor.put(`/shops/${shopId}`, payload);
}
}

export default new ShopService();
28 changes: 28 additions & 0 deletions src/apis/services/userService.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { AxiosResponse } from "axios";
import {
SignupPayload,
SignupResponse,
UserResponse,
UpdateUserPayload,
} from "src/types";

import requestor from "../client/requestor";

class UserService {
postUser(payload: SignupPayload): Promise<AxiosResponse<SignupResponse>> {
return requestor.post("/users", payload);
}

getUser(userId: string): Promise<AxiosResponse<UserResponse>> {
return requestor.get(`/users/${userId}`);
}

putUser(
userId: string,
payload: UpdateUserPayload,
): Promise<AxiosResponse<UserResponse>> {
return requestor.put(`/users/${userId}`, payload);
}
}

export default new UserService();
24 changes: 24 additions & 0 deletions src/types/alert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { ApplicationStatus } from "./application";
import type { ApiWrapper, ApiPaged } from "./common";
import type { NoticeSummary } from "./notice";
import type { ShopSummary } from "./shop";

export type AlertResult = "accepted" | "rejected";

export interface ApplicationMini {
id: string;
status: Exclude<ApplicationStatus, "canceled">;
}

export interface AlertItem {
id: string;
createdAt: string;
result: AlertResult;
read: boolean;
application: ApiWrapper<ApplicationMini>;
shop: ApiWrapper<ShopSummary>;
notice: ApiWrapper<NoticeSummary>;
Copy link
Collaborator

Choose a reason for hiding this comment

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

정의해주신 ApiWrapper 타입의 구조는 아래와 같아요.

export interface ApiWrapper<T> {
  item: T;
  links: unknown[];
}

하지만 API 노션 문서를 확인해보면
GET /users/{user_id}/alerts의 응답 데이터 형태는 아래와 같아요.

{
  "offset": "number",
  "limit": "number",
  "items": [
    {
      "item": {
        "id": "string",
        "createdAt": "string",
        "result": "accepted" | "rejected",
        "read": "boolean",
        "application": {
          "item": { ... },
          "href": "string" // 속성명이 links가 아님
        },
        "shop": {
          "item": { ... },
          "href": "string" // 속성명이 links가 아님
        },
        "notice": {
          "item": { ... },
          "href": "string" // 속성명이 links가 아님
        },
        "links": [...]
      }
    },
    "links": [...]
  ]
}

ApiWrapper 제네릭 타입과 실제 데이터 타입이 일치하지 않는 것 같아요 🥲 (links -> href 변경 필요)

}

export type AlertResponse = ApiWrapper<AlertItem>;
export type AlertListResponse = ApiPaged<AlertItem>;
22 changes: 22 additions & 0 deletions src/types/application.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { ApiWrapper, ApiPaged } from "./common";
import type { NoticeSummary } from "./notice";
import type { ShopSummary } from "./shop";
import type { UserSummary } from "./user";

export type ApplicationStatus =
| "pending"
| "accepted"
| "rejected"
| "canceled";

export interface ApplicationItem {
id: string;
status: ApplicationStatus;
createdAt: string;
user: ApiWrapper<UserSummary>;
shop: ApiWrapper<ShopSummary>;
notice: ApiWrapper<NoticeSummary>;
}

export type ApplicationResponse = ApiWrapper<ApplicationItem>;
export type ApplicationListResponse = ApiPaged<ApplicationItem>;
Loading